Skip to content

Instantly share code, notes, and snippets.

@risent
Created December 13, 2024 08:41
Show Gist options
  • Save risent/7851c6f6c0a1eacd256f80e3b6e9db6c to your computer and use it in GitHub Desktop.
Save risent/7851c6f6c0a1eacd256f80e3b6e9db6c to your computer and use it in GitHub Desktop.
A simple JSON parser write in Golang
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