Created
December 13, 2024 08:41
-
-
Save risent/7851c6f6c0a1eacd256f80e3b6e9db6c to your computer and use it in GitHub Desktop.
A simple JSON parser write in Golang
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"fmt" | |
"strconv" | |
"strings" | |
"unicode" | |
) | |
type JSONValue interface{} | |
type JSONObject map[string]JSONValue | |
type JSONArray []JSONValue | |
// Parser struct holds the input JSON string and the current parsing position. | |
type Parser struct { | |
input string | |
current int | |
} | |
// NewParser creates a new Parser instance. | |
func NewParser(input string)(*Parser) { | |
return &Parser{input: input, current: 0} | |
} | |
// peek returns the character at the current position, without advancing. | |
func (p *Parser) peek() rune { | |
// fmt.Printf("current: %d, peek: %c\n", p.current, p.input[p.current]) | |
if p.current >= len(p.input) { | |
return 0 // End of input | |
} | |
char := rune(p.input[p.current]) | |
return char | |
} | |
// advance moves the current position and returns the character that was previously at current | |
func (p *Parser) advance() rune { | |
if p.current > len(p.input) { | |
return 0 // End of input | |
} | |
char := rune(p.input[p.current]) | |
p.current++ | |
return char | |
} | |
// skipWhitespace consumes any whitespace character | |
func (p *Parser) skipWhitespace() { | |
for p.peek() != 0 && unicode.IsSpace(p.peek()) { | |
p.advance() | |
} | |
} | |
// parseString parses a JSON string value. | |
func (p *Parser) parseString() (string, error) { | |
p.advance() // Consume the opening quote | |
var sb strings.Builder | |
for p.peek() != 0 && p.peek() != '"' { | |
if p.peek() == '\\' { | |
p.advance() | |
switch p.peek() { | |
case 'n': | |
sb.WriteRune('\n') | |
case 'r': | |
sb.WriteRune('\r') | |
case 't': | |
sb.WriteRune('\t') | |
case '"': | |
sb.WriteRune('"') | |
case '\\': | |
sb.WriteRune('\\') | |
case 'u': | |
p.advance() // consume 'u' | |
var hex string = "" | |
for i := 0; i < 4; i++ { | |
if p.peek() == 0 { | |
return "", fmt.Errorf("invalid unicode charactor format") | |
} | |
hex += string(p.advance()) | |
} | |
fmt.Printf("unicode: %s, char: %c\n", hex, p.input[p.current]) | |
val, err := strconv.ParseUint(hex, 16, 32) | |
if err != nil { | |
return "", fmt.Errorf("invalid unicode character format") | |
} | |
sb.WriteRune(rune(val)) | |
default: | |
return "", fmt.Errorf("invalid escape sequence: \\%c", p.peek()) | |
} | |
} else { | |
sb.WriteRune(p.peek()) | |
p.advance() | |
} | |
} | |
if p.peek() == 0 { | |
return "", fmt.Errorf("unterminated string") | |
} | |
p.advance() // Consume the closing quote | |
return sb.String(), nil | |
} | |
// parseNumber parses a JSON number value. | |
func (p *Parser) parseNumber() (float64, error) { | |
var sb strings.Builder | |
for p.peek() != 0 && (unicode.IsDigit(p.peek()) || p.peek() == '.' || p.peek() == '-' || p.peek() == 'e' || p.peek() == 'E' || p.peek() == '+') { | |
sb.WriteRune(p.peek()) | |
p.advance() | |
} | |
numStr := sb.String() | |
num, err := strconv.ParseFloat(numStr, 64) | |
if err != nil { | |
return 0, fmt.Errorf("invalid number: %s", numStr) | |
} | |
return num, nil | |
} | |
// parseBoolean parses a JSON boolean value | |
func (p *Parser) parseBoolean() (bool, error) { | |
if strings.HasPrefix(p.input[p.current:], "true") { | |
p.current += 4 | |
return true, nil | |
} | |
if strings.HasPrefix(p.input[p.current:], "false") { | |
p.current += 4 | |
return false, nil | |
} | |
return false, fmt.Errorf("invalid boolean") | |
} | |
// parseNull parses a JSON null value | |
func (p *Parser) parseNull() (interface{}, error) { | |
if strings.HasPrefix(p.input[p.current:], "null") { | |
p.current += 4 | |
return nil, nil | |
} | |
return false, fmt.Errorf("invalid null value") | |
} | |
// parseArray parse a JSON array value | |
func (p *Parser) parseArray() (JSONArray, error) { | |
p.advance() // Consume the opening bracket | |
p.skipWhitespace() | |
var array JSONArray | |
for p.peek() != 0 && p.peek() != ']' { | |
val, err := p.parseValue() | |
if err != nil { | |
return nil, err | |
} | |
array = append(array, val) | |
p.skipWhitespace() | |
if p.peek() == ',' { | |
p.advance() // Consume the comma | |
p.skipWhitespace() | |
} | |
} | |
if p.peek() == 0 { | |
return nil, fmt.Errorf("unterminated array") | |
} | |
p.advance() // Consume the closing brace | |
return array, nil | |
} | |
// parseObject parse a JSON object value. | |
func (p *Parser) parseObject() (JSONObject, error) { | |
p.advance() | |
p.skipWhitespace() | |
obj := make(JSONObject) | |
for p.peek() != 0 && p.peek() != '}' { | |
key, err := p.parseString() | |
if err != nil { | |
return nil, fmt.Errorf("invalid object key: %s", err) | |
} | |
p.skipWhitespace() | |
if p.peek() != ':' { | |
return nil, fmt.Errorf("expected colon after object key") | |
} | |
p.advance() | |
p.skipWhitespace() | |
val, err := p.parseValue() | |
if err != nil { | |
return nil, fmt.Errorf("invalid object value: %w", err) | |
} | |
obj[key] = val | |
p.skipWhitespace() | |
if p.peek() == ',' { | |
p.advance() // Consume the comma | |
p.skipWhitespace() | |
} | |
} | |
if p.peek() == 0 { | |
return nil, fmt.Errorf("unterminated object") | |
} | |
p.advance() // Consume the closing brace | |
return obj, nil | |
} | |
// parseValue parses any JSON vale, dispatching to the appropriate parser. | |
func (p *Parser) parseValue() (JSONValue, error) { | |
// fmt.Printf("char: %c\n", p.peek()) | |
p.skipWhitespace() | |
switch p.peek() { | |
case '"': | |
return p.parseString() | |
case 'n': | |
return p.parseNull() | |
case 't', 'f': | |
return p.parseBoolean() | |
case '[': | |
return p.parseArray() | |
case '{': | |
return p.parseObject() | |
case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': | |
return p.parseNumber() | |
default: | |
return nil, fmt.Errorf("invalid JSON value") | |
} | |
} | |
// Parse initiates the JSON parsing and returns the final JSONValue | |
func (p *Parser) Parse() (JSONValue, error) { | |
val, err := p.parseValue() | |
// fmt.Printf("val: %v\n", val) | |
if err != nil { | |
return nil, err | |
} | |
p.skipWhitespace() | |
if p.peek() != 0 { | |
return nil, fmt.Errorf("trailing characters after the JSON value") | |
} | |
return val, nil | |
} | |
func main() { | |
jsonString := `{ | |
"name": "John Doe", | |
"age": 30, | |
"is_active": true, | |
"address": null, | |
"grades": [ 90, 85, 92.5 ], | |
"additional_info" : { | |
"city" : "New York", | |
"country" : "USA" | |
}, | |
"unicode": "hello \u0041" | |
}` | |
parser := NewParser(jsonString) | |
value, err := parser.Parse() | |
if err != nil { | |
fmt.Println("Error:", err) | |
return | |
} | |
fmt.Println("Parsed JSON:", value) | |
//Type assertions to get actual types. | |
if obj, ok := value.(JSONObject); ok { | |
fmt.Println("Name: ", obj["name"]) | |
fmt.Println("Age: ", obj["age"]) | |
fmt.Println("Grades:", obj["grades"]) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment