all repos

json2go @ fc9c7dff8393608d5d61f0e8f5274ad0904c93c4

convert json to go type annotations
19 files changed, 1508 insertions(+), 383 deletions(-)
refactor: use an actual parser instead of reflection

* implement lexer

* implement parser

* implement transpiler

* drop old implementation

* structName can contain unicode characters

* setup fuzzer for the parser

* add benchmarks

* reduce allocations
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed by: GitHub noreply@github.com
Committed at: 2026-05-26 14:07:11 +0300
Parent: 34a739e
M .github/workflows/go.yml
···
        8
        8
             paths: ["**.go", "go.mod", "go.sum"]

      
        9
        9
         

      
        10
        10
         jobs:

      
        11
        
        -  release:

      
        
        11
        +  test:

      
        12
        12
             runs-on: ubuntu-latest

      
        13
        13
             steps:

      
        14
        14
               - uses: actions/checkout@v5

      ···
        24
        24
         

      
        25
        25
               - name: test

      
        26
        26
                 run: go test -v ./...

      
        
        27
        +

      
        
        28
        +  fuzz:

      
        
        29
        +    runs-on: ubuntu-latest

      
        
        30
        +    steps:

      
        
        31
        +      - uses: actions/checkout@v5

      
        
        32
        +

      
        
        33
        +      - name: setup go

      
        
        34
        +        uses: actions/setup-go@v5

      
        
        35
        +        with:

      
        
        36
        +          go-version-file: go.mod

      
        
        37
        +          cache-dependency-path: go.mod

      
        
        38
        +

      
        
        39
        +      - name: fuzz parser

      
        
        40
        +        run: go test -fuzz=FuzzParser -fuzztime=60s ./

      
        
        41
        +

      
        
        42
        +  bench:

      
        
        43
        +    runs-on: ubuntu-latest

      
        
        44
        +    steps:

      
        
        45
        +      - uses: actions/checkout@v5

      
        
        46
        +

      
        
        47
        +      - name: setup go

      
        
        48
        +        uses: actions/setup-go@v5

      
        
        49
        +        with:

      
        
        50
        +          go-version-file: go.mod

      
        
        51
        +          cache-dependency-path: go.mod

      
        
        52
        +

      
        
        53
        +      - name: run benchmarks

      
        
        54
        +        run: go test -bench=. -benchmem -v ./

      
M cmd/json2go/main.go
···
        11
        11
         

      
        12
        12
         func main() {

      
        13
        13
         	typeName := flag.String("type", "AutoGenerated", "a name for generated type")

      
        
        14
        +	noTags := flag.Bool("no-json-tags", false, "do not include json tags on struct fields")

      
        14
        15
         	showHelp := flag.Bool("help", false, "show help")

      
        15
        16
         	flag.Parse()

      
        16
        17
         

      ···
        46
        47
         		os.Exit(1)

      
        47
        48
         	}

      
        48
        49
         

      
        49
        
        -	transformer := json2go.NewTransformer()

      
        50
        
        -	type_, err := transformer.Transform(*typeName, input)

      
        
        50
        +	type_, err := json2go.Transform(*typeName, input, !*noTags)

      
        51
        51
         	if err != nil {

      
        52
        52
         		fmt.Fprintf(os.Stderr, "Failed to transform json to type annotation: %v\n", err)

      
        53
        53
         		os.Exit(1)

      ···
        63
        63
         Usage:

      
        64
        64
         	echo '{"json": "here"}' | json2go

      
        65
        65
         	echo '{"json": "here"}' | json2go -type=MyTypeName

      
        66
        
        -	json2go -type=MyTypeName '{"json": "here"}'`[1:])

      
        
        66
        +	json2go -type=MyTypeName '{"json": "here"}'

      
        
        67
        +	json2go -no-json-tags '{"json": "here"}'

      
        
        68
        +

      
        
        69
        +Flags:

      
        
        70
        +	-type=NAME         Type name for root type (default: AutoGenerated)

      
        
        71
        +	-no-json-tags      Omit json struct tags`[1:])

      
        67
        72
         }

      
M go.mod
···
        1
        1
         module olexsmir.xyz/json2go

      
        2
        2
         

      
        3
        3
         go 1.25.3

      
        
        4
        +

      
        
        5
        +tool golang.org/x/tools/cmd/stringer

      
        
        6
        +

      
        
        7
        +require (

      
        
        8
        +	golang.org/x/mod v0.36.0 // indirect

      
        
        9
        +	golang.org/x/sync v0.20.0 // indirect

      
        
        10
        +	golang.org/x/tools v0.45.0 // indirect

      
        
        11
        +)

      
A go.sum
···
        
        1
        +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=

      
        
        2
        +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=

      
        
        3
        +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=

      
        
        4
        +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=

      
        
        5
        +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=

      
        
        6
        +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=

      
        
        7
        +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=

      
        
        8
        +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=

      
M json2go.go
···
        1
        1
         package json2go

      
        2
        2
         

      
        3
        3
         import (

      
        4
        
        -	"encoding/json"

      
        5
        4
         	"errors"

      
        6
        
        -	"fmt"

      
        7
        
        -	"regexp"

      
        8
        
        -	"sort"

      
        9
        
        -	"strings"

      
        
        5
        +	"unicode"

      
        
        6
        +	"unicode/utf8"

      
        
        7
        +	"unsafe"

      
        10
        8
         )

      
        11
        9
         

      
        12
        
        -var identRe = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)

      
        13
        10
         var (

      
        14
        
        -	ErrInvalidJSON       = errors.New("invalid json")

      
        
        11
        +	// ErrInvalidJSON json input could not be parsed.

      
        
        12
        +	ErrInvalidJSON = errors.New("invalid json")

      
        
        13
        +

      
        
        14
        +	// ErrInvalidStructName struct name provided is not a valid Go identifier.

      
        15
        15
         	ErrInvalidStructName = errors.New("invalid struct name")

      
        16
        16
         )

      
        17
        17
         

      
        18
        
        -type Transformer struct {

      
        19
        
        -	structName    string

      
        20
        
        -	currentIndent int

      
        21
        
        -}

      
        22
        
        -

      
        23
        
        -func NewTransformer() *Transformer {

      
        24
        
        -	return &Transformer{}

      
        25
        
        -}

      
        26
        
        -

      
        27
        
        -// Transform transforms provided json string into go type annotation

      
        28
        
        -func (t *Transformer) Transform(structName, jsonStr string) (string, error) {

      
        29
        
        -	if !identRe.MatchString(structName) {

      
        
        18
        +// Transform converts a JSON string to Go struct type definitions.

      
        
        19
        +//

      
        
        20
        +// The structName must be a valid Go identifier.

      
        
        21
        +// Set includeTags to true to generate `json:"field_name"` tags on struct fields.

      
        
        22
        +// Returns the Go code as a string, or an error if JSON parsing fails.

      
        
        23
        +func Transform(structName, jsonStr string, includeTags bool) (string, error) {

      
        
        24
        +	if !isValidIdentifier(structName) {

      
        30
        25
         		return "", ErrInvalidStructName

      
        31
        26
         	}

      
        32
        27
         

      
        33
        
        -	t.structName = structName

      
        34
        
        -	t.currentIndent = 0

      
        35
        
        -

      
        36
        
        -	var input any

      
        37
        
        -	if err := json.Unmarshal([]byte(jsonStr), &input); err != nil {

      
        
        28
        +	input := unsafe.Slice(unsafe.StringData(jsonStr), len(jsonStr))

      
        
        29
        +	lexer := NewLexer(input)

      
        
        30
        +	parser := NewParser(lexer)

      
        
        31
        +	v, err := parser.Parse()

      
        
        32
        +	if err != nil {

      
        38
        33
         		return "", errors.Join(ErrInvalidJSON, err)

      
        39
        34
         	}

      
        40
        35
         

      
        41
        
        -	type_ := t.getTypeAnnotation(structName, input)

      
        42
        
        -	return type_, nil

      
        
        36
        +	return NewTranspiler().Transpile(structName, v, includeTags)

      
        43
        37
         }

      
        44
        38
         

      
        45
        
        -func (t *Transformer) getTypeAnnotation(typeName string, input any) string {

      
        46
        
        -	switch v := input.(type) {

      
        47
        
        -	case map[string]any:

      
        48
        
        -		return fmt.Sprintf("type %s %s", typeName, t.buildStruct(v))

      
        49
        
        -

      
        50
        
        -	case []any:

      
        51
        
        -		if len(v) == 0 {

      
        52
        
        -			return fmt.Sprintf("type %s []any", typeName)

      
        53
        
        -		}

      
        54
        
        -

      
        55
        
        -		type_ := t.getGoType(typeName+"Item", v[0])

      
        56
        
        -		return fmt.Sprintf("type %s []%s", typeName, type_)

      
        57
        
        -

      
        58
        
        -	case string:

      
        59
        
        -		return fmt.Sprintf("type %s string", typeName)

      
        60
        
        -

      
        61
        
        -	case float64:

      
        62
        
        -		if float64(int(v)) == v {

      
        63
        
        -			return fmt.Sprintf("type %s int", typeName)

      
        64
        
        -		}

      
        65
        
        -		return fmt.Sprintf("type %s float64", typeName)

      
        66
        
        -

      
        67
        
        -	case bool:

      
        68
        
        -		return fmt.Sprintf("type %s bool", typeName)

      
        69
        
        -

      
        70
        
        -	default:

      
        71
        
        -		return fmt.Sprintf("type %s any", typeName)

      
        72
        
        -

      
        73
        
        -	}

      
        74
        
        -}

      
        75
        
        -

      
        76
        
        -func (t *Transformer) buildStruct(input map[string]any) string {

      
        77
        
        -	var fields strings.Builder

      
        78
        
        -	for _, f := range mapToStructInput(input) {

      
        79
        
        -		fieldName := t.toGoFieldName(f.field)

      
        80
        
        -		if fieldName == "" {

      
        81
        
        -			fieldName = "NotNamedField"

      
        82
        
        -			f.field = "NotNamedField"

      
        83
        
        -		}

      
        84
        
        -

      
        85
        
        -		// increase indentation in case of building new struct

      
        86
        
        -		t.currentIndent++

      
        87
        
        -		fieldType := t.getGoType(fieldName, f.type_)

      
        88
        
        -		t.currentIndent--

      
        89
        
        -

      
        90
        
        -		jsonTag := fmt.Sprintf("`json:\"%s\"`", f.field)

      
        91
        
        -

      
        92
        
        -		indent := strings.Repeat("\t", t.currentIndent+1)

      
        93
        
        -		fields.WriteString(fmt.Sprintf(

      
        94
        
        -			"%s%s %s %s\n",

      
        95
        
        -			indent,

      
        96
        
        -			fieldName,

      
        97
        
        -			fieldType,

      
        98
        
        -			jsonTag,

      
        99
        
        -		))

      
        
        39
        +func isValidIdentifier(s string) bool {

      
        
        40
        +	if len(s) == 0 {

      
        
        41
        +		return false

      
        100
        42
         	}

      
        101
        43
         

      
        102
        
        -	return fmt.Sprintf("struct {\n%s%s}",

      
        103
        
        -		fields.String(),

      
        104
        
        -		strings.Repeat("\t", t.currentIndent))

      
        105
        
        -}

      
        106
        
        -

      
        107
        
        -func (t *Transformer) getGoType(fieldName string, value any) string {

      
        108
        
        -	switch v := value.(type) {

      
        109
        
        -	case map[string]any:

      
        110
        
        -		return t.buildStruct(v)

      
        111
        
        -

      
        112
        
        -	case []any:

      
        113
        
        -		if len(v) == 0 {

      
        114
        
        -			return "[]any"

      
        115
        
        -		}

      
        116
        
        -

      
        117
        
        -		type_ := t.getGoType(fieldName, v[0])

      
        118
        
        -		return "[]" + type_

      
        119
        
        -

      
        120
        
        -	case float64:

      
        121
        
        -		if float64(int(v)) == v {

      
        122
        
        -			return "int"

      
        123
        
        -		}

      
        124
        
        -		return "float64"

      
        125
        
        -

      
        126
        
        -	case string:

      
        127
        
        -		return "string"

      
        128
        
        -

      
        129
        
        -	case bool:

      
        130
        
        -		return "bool"

      
        131
        
        -

      
        132
        
        -	default:

      
        133
        
        -		return "any"

      
        
        44
        +	r, size := utf8.DecodeRuneInString(s)

      
        
        45
        +	if !unicode.IsLetter(r) && r != '_' {

      
        
        46
        +		return false

      
        134
        47
         	}

      
        135
        
        -}

      
        136
        48
         

      
        137
        
        -func (t *Transformer) toGoFieldName(jsonField string) string {

      
        138
        
        -	parts := strings.Split(jsonField, "_")

      
        139
        
        -

      
        140
        
        -	var result strings.Builder

      
        141
        
        -	for _, part := range parts {

      
        142
        
        -		if part != "" {

      
        143
        
        -			if len(part) > 0 {

      
        144
        
        -				result.WriteString(strings.ToUpper(part[:1]) + part[1:])

      
        145
        
        -			}

      
        
        49
        +	for _, r := range s[size:] {

      
        
        50
        +		if !unicode.IsLetter(r) && !unicode.IsDigit(r) && r != '_' {

      
        
        51
        +			return false

      
        146
        52
         		}

      
        147
        53
         	}

      
        148
        54
         

      
        149
        
        -	return result.String()

      
        150
        
        -}

      
        151
        
        -

      
        152
        
        -type structInput struct {

      
        153
        
        -	field string

      
        154
        
        -	type_ any

      
        155
        
        -}

      
        156
        
        -

      
        157
        
        -func mapToStructInput(input map[string]any) []structInput {

      
        158
        
        -	res := make([]structInput, 0, len(input))

      
        159
        
        -	for k, v := range input {

      
        160
        
        -		res = append(res, structInput{k, v})

      
        161
        
        -	}

      
        162
        
        -

      
        163
        
        -	sort.Slice(res, func(i, j int) bool {

      
        164
        
        -		return res[i].field < res[j].field

      
        165
        
        -	})

      
        166
        
        -

      
        167
        
        -	return res

      
        
        55
        +	return true

      
        168
        56
         }

      
A json2go_bench_test.go
···
        
        1
        +package json2go

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"testing"

      
        
        5
        +)

      
        
        6
        +

      
        
        7
        +func BenchmarkTransform(b *testing.B) {

      
        
        8
        +	benches := map[string]string{

      
        
        9
        +		"simple_object":    `{"name":"alice","age":30,"active":true}`,

      
        
        10
        +		"flat_object":      `{"id":1,"first_name":"john","last_name":"doe","email":"john@example.com","phone":"+1234567890","created_at":"2024-01-01T00:00:00Z","updated_at":"2024-01-02T00:00:00Z"}`,

      
        
        11
        +		"nested_object":    `{"user":{"id":1,"name":"alice","profile":{"bio":"engineer","location":"sf","skills":["go","rust","python"]},"settings":{"theme":"dark","notifications":true}}}`,

      
        
        12
        +		"array_of_objects": `[{"id":1,"name":"alice"},{"id":2,"name":"bob"},{"id":3,"name":"charlie"}]`,

      
        
        13
        +		"mixed_types":      `{"string":"text","number":42,"decimal":3.14,"bool":true,"null_val":null,"array":[1,2,3],"object":{"nested":true}}`,

      
        
        14
        +		"deeply_nested":    `{"a":{"b":{"c":{"d":{"e":{"f":{"g":{"h":{"i":{"j":{"value":"deep"}}}}}}}}}}}`,

      
        
        15
        +		"large_array":      `[{"id":1,"val":"a"},{"id":2,"val":"b"},{"id":3,"val":"c"},{"id":4,"val":"d"},{"id":5,"val":"e"},{"id":6,"val":"f"},{"id":7,"val":"g"},{"id":8,"val":"h"},{"id":9,"val":"i"},{"id":10,"val":"j"}]`,

      
        
        16
        +		"unicode_strings":  `{"english":"hello","chinese":"你好","arabic":"مرحبا","emoji":"🚀🔥✨"}`,

      
        
        17
        +	}

      
        
        18
        +	for bname, bjson := range benches {

      
        
        19
        +		b.Run(bname+"_with_tags", func(b *testing.B) {

      
        
        20
        +			b.ReportAllocs()

      
        
        21
        +			for i := 0; i < b.N; i++ {

      
        
        22
        +				Transform("Test", bjson, true)

      
        
        23
        +			}

      
        
        24
        +		})

      
        
        25
        +

      
        
        26
        +		b.Run(bname+"_no_tags", func(b *testing.B) {

      
        
        27
        +			b.ReportAllocs()

      
        
        28
        +			for i := 0; i < b.N; i++ {

      
        
        29
        +				Transform("Test", bjson, false)

      
        
        30
        +			}

      
        
        31
        +		})

      
        
        32
        +	}

      
        
        33
        +}

      
        
        34
        +

      
        
        35
        +func BenchmarkPipeline(b *testing.B) {

      
        
        36
        +	json := `{"user":{"id":1,"name":"alice","email":"alice@example.com","profile":{"bio":"engineer","location":"sf"}}}`

      
        
        37
        +

      
        
        38
        +	b.Run("lexer_only", func(b *testing.B) {

      
        
        39
        +		b.ReportAllocs()

      
        
        40
        +		for i := 0; i < b.N; i++ {

      
        
        41
        +			lexer := NewLexer([]byte(json))

      
        
        42
        +			for {

      
        
        43
        +				tok := lexer.Next()

      
        
        44
        +				if tok.Type == EOF {

      
        
        45
        +					break

      
        
        46
        +				}

      
        
        47
        +			}

      
        
        48
        +		}

      
        
        49
        +	})

      
        
        50
        +

      
        
        51
        +	b.Run("parser_only", func(b *testing.B) {

      
        
        52
        +		b.ReportAllocs()

      
        
        53
        +		for i := 0; i < b.N; i++ {

      
        
        54
        +			lexer := NewLexer([]byte(json))

      
        
        55
        +			parser := NewParser(lexer)

      
        
        56
        +			parser.Parse()

      
        
        57
        +		}

      
        
        58
        +	})

      
        
        59
        +

      
        
        60
        +	b.Run("transpiler_only", func(b *testing.B) {

      
        
        61
        +		lexer := NewLexer([]byte(json))

      
        
        62
        +		parser := NewParser(lexer)

      
        
        63
        +		v, _ := parser.Parse()

      
        
        64
        +

      
        
        65
        +		b.ReportAllocs()

      
        
        66
        +		b.ResetTimer()

      
        
        67
        +		for i := 0; i < b.N; i++ {

      
        
        68
        +			NewTranspiler().Transpile("Test", v, true)

      
        
        69
        +		}

      
        
        70
        +	})

      
        
        71
        +

      
        
        72
        +	b.Run("full_pipeline", func(b *testing.B) {

      
        
        73
        +		b.ReportAllocs()

      
        
        74
        +		for i := 0; i < b.N; i++ {

      
        
        75
        +			Transform("Test", json, true)

      
        
        76
        +		}

      
        
        77
        +	})

      
        
        78
        +}

      
D json2go_internal_test.go
···
        1
        
        -package json2go

      
        2
        
        -

      
        3
        
        -import "testing"

      
        4
        
        -

      
        5
        
        -func TestTransformer_GetGoType(t *testing.T) {

      
        6
        
        -	tests := map[string]struct {

      
        7
        
        -		value     any

      
        8
        
        -		fieldName string

      
        9
        
        -		output    string

      
        10
        
        -	}{

      
        11
        
        -		"struct": {

      
        12
        
        -			value: map[string]any{

      
        13
        
        -				"username": "user-ovich",

      
        14
        
        -				"age":      float64(20),

      
        15
        
        -			},

      
        16
        
        -			output: "struct {" +

      
        17
        
        -				field(1, "Age", "int") +

      
        18
        
        -				field(1, "Username", "string") +

      
        19
        
        -				"\n}",

      
        20
        
        -		},

      
        21
        
        -		"empty slice": {

      
        22
        
        -			value:  make([]any, 0),

      
        23
        
        -			output: "[]any",

      
        24
        
        -		},

      
        25
        
        -		"slice of ints": {

      
        26
        
        -			value:  []any{float64(3), float64(123)},

      
        27
        
        -			output: "[]int",

      
        28
        
        -		},

      
        29
        
        -		"slice of floats": {

      
        30
        
        -			value:  []any{float64(3.4), float64(123.3)},

      
        31
        
        -			output: "[]float64",

      
        32
        
        -		},

      
        33
        
        -		"slice of strings": {

      
        34
        
        -			value:  []any{"asdf", "jalkjsd"},

      
        35
        
        -			output: "[]string",

      
        36
        
        -		},

      
        37
        
        -		"slice of bool": {

      
        38
        
        -			value:  []any{false, true, false},

      
        39
        
        -			output: "[]bool",

      
        40
        
        -		},

      
        41
        
        -		"int": {

      
        42
        
        -			value:  float64(1233),

      
        43
        
        -			output: "int",

      
        44
        
        -		},

      
        45
        
        -		"float64": {

      
        46
        
        -			value:  float64(1233.23),

      
        47
        
        -			output: "float64",

      
        48
        
        -		},

      
        49
        
        -		"bool": {

      
        50
        
        -			value:  false,

      
        51
        
        -			output: "bool",

      
        52
        
        -		},

      
        53
        
        -		"any": {

      
        54
        
        -			value:  nil,

      
        55
        
        -			output: "any",

      
        56
        
        -		},

      
        57
        
        -	}

      
        58
        
        -

      
        59
        
        -	trans := NewTransformer()

      
        60
        
        -	for tname, tt := range tests {

      
        61
        
        -		t.Run(tname, func(t *testing.T) {

      
        62
        
        -			t.Parallel()

      
        63
        
        -

      
        64
        
        -			fieldName := "field"

      
        65
        
        -			if tt.fieldName != "" {

      
        66
        
        -				fieldName = tt.fieldName

      
        67
        
        -			}

      
        68
        
        -

      
        69
        
        -			res := trans.getGoType(fieldName, tt.value)

      
        70
        
        -			assertEqual(t, tt.output, res)

      
        71
        
        -		})

      
        72
        
        -	}

      
        73
        
        -}

      
        74
        
        -

      
        75
        
        -func TestTransformer_buildStruct(t *testing.T) {

      
        76
        
        -	tests := map[string]struct {

      
        77
        
        -		input  map[string]any

      
        78
        
        -		output string

      
        79
        
        -	}{

      
        80
        
        -		"simple struct": {

      
        81
        
        -			input: map[string]any{

      
        82
        
        -				// only one value, because of the inconsistent ordering of maps

      
        83
        
        -				"active": true,

      
        84
        
        -			},

      
        85
        
        -			output: "struct {" +

      
        86
        
        -				field(1, "Active", "bool", "active") +

      
        87
        
        -				"\n}",

      
        88
        
        -		},

      
        89
        
        -		"with no named field": {

      
        90
        
        -			input: map[string]any{"": "user"},

      
        91
        
        -			output: "struct {" +

      
        92
        
        -				field(1, "NotNamedField", "string", "NotNamedField") +

      
        93
        
        -				"\n}",

      
        94
        
        -		},

      
        95
        
        -	}

      
        96
        
        -

      
        97
        
        -	trans := NewTransformer()

      
        98
        
        -	for tname, tt := range tests {

      
        99
        
        -		t.Run(tname, func(t *testing.T) {

      
        100
        
        -			t.Parallel()

      
        101
        
        -

      
        102
        
        -			res := trans.buildStruct(tt.input)

      
        103
        
        -			assertEqual(t, tt.output, res)

      
        104
        
        -		})

      
        105
        
        -	}

      
        106
        
        -}

      
        107
        
        -

      
        108
        
        -func TestTransformer_getTypeAnnotation(t *testing.T) {

      
        109
        
        -	c := "type Typeich "

      
        110
        
        -	tests := map[string]struct {

      
        111
        
        -		input  any

      
        112
        
        -		output string

      
        113
        
        -	}{

      
        114
        
        -		"struct": {

      
        115
        
        -			input: map[string]any{"field": false},

      
        116
        
        -			output: c + "struct {" +

      
        117
        
        -				field(1, "Field", "bool") + "\n}",

      
        118
        
        -		},

      
        119
        
        -		"slice": {

      
        120
        
        -			input:  []any{"asdf", "jkl;"},

      
        121
        
        -			output: c + "[]string",

      
        122
        
        -		},

      
        123
        
        -		"empty slice": {

      
        124
        
        -			input:  make([]any, 0),

      
        125
        
        -			output: c + "[]any",

      
        126
        
        -		},

      
        127
        
        -		"string": {

      
        128
        
        -			input:  "asdf",

      
        129
        
        -			output: c + "string",

      
        130
        
        -		},

      
        131
        
        -		"int": {

      
        132
        
        -			input:  float64(123),

      
        133
        
        -			output: c + "int",

      
        134
        
        -		},

      
        135
        
        -		"float64": {

      
        136
        
        -			input:  float64(123.69),

      
        137
        
        -			output: c + "float64",

      
        138
        
        -		},

      
        139
        
        -		"bool": {

      
        140
        
        -			input:  true,

      
        141
        
        -			output: c + "bool",

      
        142
        
        -		},

      
        143
        
        -		"any": {

      
        144
        
        -			input:  nil,

      
        145
        
        -			output: c + "any",

      
        146
        
        -		},

      
        147
        
        -	}

      
        148
        
        -

      
        149
        
        -	trans := NewTransformer()

      
        150
        
        -	for tname, tt := range tests {

      
        151
        
        -		t.Run(tname, func(t *testing.T) {

      
        152
        
        -			t.Parallel()

      
        153
        
        -

      
        154
        
        -			res := trans.getTypeAnnotation("Typeich", tt.input)

      
        155
        
        -			assertEqual(t, tt.output, res)

      
        156
        
        -		})

      
        157
        
        -	}

      
        158
        
        -}

      
        159
        
        -

      
        160
        
        -func TestTransformer_toGoFieldName(t *testing.T) {

      
        161
        
        -	tests := map[string]string{

      
        162
        
        -		"input":         "Input",

      
        163
        
        -		"Input":         "Input",

      
        164
        
        -		"long_name":     "LongName",

      
        165
        
        -		"a_lot_of_____": "ALotOf",

      
        166
        
        -		"__name":        "Name",

      
        167
        
        -	}

      
        168
        
        -

      
        169
        
        -	trans := NewTransformer()

      
        170
        
        -	for input, output := range tests {

      
        171
        
        -		t.Run(input, func(t *testing.T) {

      
        172
        
        -			t.Parallel()

      
        173
        
        -

      
        174
        
        -			res := trans.toGoFieldName(input)

      
        175
        
        -			assertEqual(t, output, res)

      
        176
        
        -		})

      
        177
        
        -	}

      
        178
        
        -}

      
        179
        
        -

      
        180
        
        -func TestMapToStructInput(t *testing.T) {

      
        181
        
        -	inp := map[string]any{

      
        182
        
        -		"field1": nil,

      
        183
        
        -		"field2": true,

      
        184
        
        -		"a":      123,

      
        185
        
        -		"user":   map[string]any{},

      
        186
        
        -	}

      
        187
        
        -

      
        188
        
        -	assertEqual(t, mapToStructInput(inp), []structInput{

      
        189
        
        -		{"a", 123},

      
        190
        
        -		{"field1", nil},

      
        191
        
        -		{"field2", true},

      
        192
        
        -		{"user", map[string]any{}},

      
        193
        
        -	})

      
        194
        
        -}

      
M json2go_test.go
···
        21
        21
         	return fmt.Sprintf("\n%s%s %s `json:\"%s\"`", indent, name, type_, tag)

      
        22
        22
         }

      
        23
        23
         

      
        24
        
        -func TestTransformer_Transform(t *testing.T) {

      
        
        24
        +func TestTransform(t *testing.T) {

      
        25
        25
         	tests := map[string]struct {

      
        26
        26
         		input      string

      
        27
        
        -		output     string

      
        
        27
        +		check      func(t *testing.T, result string)

      
        28
        28
         		structName string

      
        29
        29
         		err        error

      
        30
        30
         	}{

      
        31
        31
         		"simple object": {

      
        32
        32
         			input: `{"name": "Olex", "active": true, "age": 420}`,

      
        33
        
        -			output: "type Out struct {" +

      
        34
        
        -				field(1, "Active", "bool") +

      
        35
        
        -				field(1, "Age", "int") +

      
        36
        
        -				field(1, "Name", "string") +

      
        37
        
        -				"\n}",

      
        
        33
        +			check: func(t *testing.T, result string) {

      
        
        34
        +				if !strings.Contains(result, "type Out struct") {

      
        
        35
        +					t.Errorf("missing Out struct")

      
        
        36
        +				}

      
        
        37
        +				if !strings.Contains(result, "Name string `json:\"name\"`") {

      
        
        38
        +					t.Errorf("missing Name field")

      
        
        39
        +				}

      
        
        40
        +				if !strings.Contains(result, "Active bool `json:\"active\"`") {

      
        
        41
        +					t.Errorf("missing Active field")

      
        
        42
        +				}

      
        
        43
        +				if !strings.Contains(result, "Age int `json:\"age\"`") {

      
        
        44
        +					t.Errorf("missing Age field")

      
        
        45
        +				}

      
        
        46
        +			},

      
        38
        47
         		},

      
        39
        48
         		"invalid json": {

      
        40
        49
         			err:   ErrInvalidJSON,

      ···
        54
        63
         		},

      
        55
        64
         		"snake_case to CamelCase": {

      
        56
        65
         			input: `{"first_name": "Bob", "last_name": "Bobberson"}`,

      
        57
        
        -			output: "type Out struct {" +

      
        58
        
        -				field(1, "FirstName", "string", "first_name") +

      
        59
        
        -				field(1, "LastName", "string", "last_name") +

      
        60
        
        -				"\n}",

      
        
        66
        +			check: func(t *testing.T, result string) {

      
        
        67
        +				if !strings.Contains(result, "FirstName string `json:\"first_name\"`") {

      
        
        68
        +					t.Errorf("missing FirstName field")

      
        
        69
        +				}

      
        
        70
        +				if !strings.Contains(result, "LastName string `json:\"last_name\"`") {

      
        
        71
        +					t.Errorf("missing LastName field")

      
        
        72
        +				}

      
        
        73
        +			},

      
        61
        74
         		},

      
        62
        75
         		"nested object and array": {

      
        63
        76
         			input: `{"user": {"name": "Alice", "score": 95.5}, "tags": ["go", "json"]}`,

      
        64
        
        -			output: "type Out struct {" +

      
        65
        
        -				field(1, "Tags", "[]string") +

      
        66
        
        -				field(1, "User", "struct {") +

      
        67
        
        -				field(2, "Name", "string") +

      
        68
        
        -				field(2, "Score", "float64") +

      
        69
        
        -				"\n\t} `json:\"user\"`" +

      
        70
        
        -				"\n}",

      
        
        77
        +			check: func(t *testing.T, result string) {

      
        
        78
        +				if !strings.Contains(result, "type Out struct") {

      
        
        79
        +					t.Errorf("missing Out struct")

      
        
        80
        +				}

      
        
        81
        +				if !strings.Contains(result, "Tags []string") {

      
        
        82
        +					t.Errorf("missing Tags field")

      
        
        83
        +				}

      
        
        84
        +				if !strings.Contains(result, "User OutUser") {

      
        
        85
        +					t.Errorf("missing User field")

      
        
        86
        +				}

      
        
        87
        +				if !strings.Contains(result, "type OutUser struct") {

      
        
        88
        +					t.Errorf("missing OutUser struct")

      
        
        89
        +				}

      
        
        90
        +			},

      
        71
        91
         		},

      
        72
        92
         		"empty nested object": {

      
        73
        93
         			input: `{"user": {}}`,

      
        74
        
        -			output: "type Out struct {" +

      
        75
        
        -				field(1, "User", "struct {") +

      
        76
        
        -				"\n\t} `json:\"user\"`" +

      
        77
        
        -				"\n}",

      
        
        94
        +			check: func(t *testing.T, result string) {

      
        
        95
        +				if !strings.Contains(result, "type Out struct") {

      
        
        96
        +					t.Errorf("missing Out struct")

      
        
        97
        +				}

      
        
        98
        +				if !strings.Contains(result, "type OutUser struct") {

      
        
        99
        +					t.Errorf("missing OutUser struct")

      
        
        100
        +				}

      
        
        101
        +			},

      
        78
        102
         		},

      
        79
        103
         		"array of object": {

      
        80
        104
         			input: `[{"name": "John"}, {"name": "Jane"}]`,

      
        81
        
        -			output: "type Out []struct {" +

      
        82
        
        -				field(1, "Name", "string") +

      
        83
        
        -				"\n}",

      
        
        105
        +			check: func(t *testing.T, result string) {

      
        
        106
        +				if !strings.Contains(result, "type Out []OutItem") {

      
        
        107
        +					t.Errorf("missing Out array type")

      
        
        108
        +				}

      
        
        109
        +				if !strings.Contains(result, "type OutItem struct") {

      
        
        110
        +					t.Errorf("missing OutItem struct")

      
        
        111
        +				}

      
        
        112
        +			},

      
        84
        113
         		},

      
        85
        114
         		"empty array": {

      
        86
        115
         			input: `{"items": []}`,

      
        87
        
        -			output: "type Out struct {" +

      
        88
        
        -				field(1, "Items", "[]any") +

      
        89
        
        -				"\n}",

      
        
        116
        +			check: func(t *testing.T, result string) {

      
        
        117
        +				if !strings.Contains(result, "Items []any `json:\"items\"`") {

      
        
        118
        +					t.Errorf("missing Items field")

      
        
        119
        +				}

      
        
        120
        +			},

      
        90
        121
         		},

      
        91
        122
         		"null": {

      
        92
        123
         			input: `{"item": null}`,

      
        93
        
        -			output: `type Out struct {` +

      
        94
        
        -				field(1, "Item", "any") +

      
        95
        
        -				"\n}",

      
        
        124
        +			check: func(t *testing.T, result string) {

      
        
        125
        +				if !strings.Contains(result, "Item any `json:\"item\"`") {

      
        
        126
        +					t.Errorf("missing Item field")

      
        
        127
        +				}

      
        
        128
        +			},

      
        96
        129
         		},

      
        97
        130
         		"numbers": {

      
        98
        131
         			input: `{"pos": 123, "neg": -321, "float": 420.69}`,

      
        99
        
        -			output: "type Out struct {" +

      
        100
        
        -				field(1, "Float", "float64") +

      
        101
        
        -				field(1, "Neg", "int") +

      
        102
        
        -				field(1, "Pos", "int") +

      
        103
        
        -				"\n}",

      
        
        132
        +			check: func(t *testing.T, result string) {

      
        
        133
        +				if !strings.Contains(result, "Pos int `json:\"pos\"`") {

      
        
        134
        +					t.Errorf("missing Pos field")

      
        
        135
        +				}

      
        
        136
        +				if !strings.Contains(result, "Neg int `json:\"neg\"`") {

      
        
        137
        +					t.Errorf("missing Neg field")

      
        
        138
        +				}

      
        
        139
        +				if !strings.Contains(result, "Float float64 `json:\"float\"`") {

      
        
        140
        +					t.Errorf("missing Float field")

      
        
        141
        +				}

      
        
        142
        +			},

      
        104
        143
         		},

      
        105
        144
         	}

      
        106
        145
         

      
        107
        
        -	trans := NewTransformer()

      
        108
        146
         	for tname, tt := range tests {

      
        109
        147
         		t.Run(tname, func(t *testing.T) {

      
        110
        148
         			sn := "Out"

      ···
        112
        150
         				sn = tt.structName

      
        113
        151
         			}

      
        114
        152
         

      
        115
        
        -			result, err := trans.Transform(sn, tt.input)

      
        
        153
        +			result, err := Transform(sn, tt.input, true)

      
        116
        154
         			assertEqualErr(t, tt.err, err)

      
        117
        
        -			assertEqual(t, tt.output, result)

      
        
        155
        +			if tt.check != nil {

      
        
        156
        +				tt.check(t, result)

      
        
        157
        +			}

      
        118
        158
         		})

      
        119
        159
         	}

      
        120
        160
         }

      
A lexer.go
···
        
        1
        +package json2go

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"unicode/utf8"

      
        
        5
        +	"unsafe"

      
        
        6
        +)

      
        
        7
        +

      
        
        8
        +type Lexer struct {

      
        
        9
        +	input  []byte

      
        
        10
        +	ch     rune // current rune (0 == EOF)

      
        
        11
        +	chSize int  // byte size of [ch]

      
        
        12
        +	pos    int  // current byte offset (points at [ch])

      
        
        13
        +	rpos   int  // next byte offset to read (one ahead of [pos])

      
        
        14
        +	col    int  // current column (1-based)

      
        
        15
        +	line   int  // current line (1-based)

      
        
        16
        +}

      
        
        17
        +

      
        
        18
        +func NewLexer(input []byte) *Lexer {

      
        
        19
        +	l := &Lexer{input: input, line: 1}

      
        
        20
        +	l.advance()

      
        
        21
        +	if l.ch == '\uFEFF' { // start of the input

      
        
        22
        +		l.advance()

      
        
        23
        +	}

      
        
        24
        +	return l

      
        
        25
        +}

      
        
        26
        +

      
        
        27
        +// Next returns the next token from the input.

      
        
        28
        +// Returns EOF when input is exhausted.

      
        
        29
        +func (l *Lexer) Next() Token {

      
        
        30
        +	switch {

      
        
        31
        +	case l.ch == 0:

      
        
        32
        +		return Token{EOF, ""}

      
        
        33
        +	case l.ch == '\n', l.ch == '\r':

      
        
        34
        +		l.advance()

      
        
        35
        +		return Token{NEWLINE, "\n"}

      
        
        36
        +	case l.ch == ' ', l.ch == '\t':

      
        
        37
        +		offset := l.pos

      
        
        38
        +		for l.ch == ' ' || l.ch == '\t' {

      
        
        39
        +			l.advance()

      
        
        40
        +		}

      
        
        41
        +		return Token{INDENT, sliceString(l.input, offset, l.pos)}

      
        
        42
        +	case l.ch == '/':

      
        
        43
        +		return l.lexComment()

      
        
        44
        +	case l.ch == '"':

      
        
        45
        +		return l.lexString()

      
        
        46
        +	case l.ch == ':':

      
        
        47
        +		l.advance()

      
        
        48
        +		return Token{COLON, ":"}

      
        
        49
        +	case l.ch == ',':

      
        
        50
        +		l.advance()

      
        
        51
        +		return Token{COMMA, ","}

      
        
        52
        +	case l.ch == '[':

      
        
        53
        +		l.advance()

      
        
        54
        +		return Token{LBRACKET, "["}

      
        
        55
        +	case l.ch == ']':

      
        
        56
        +		l.advance()

      
        
        57
        +		return Token{RBRACKET, "]"}

      
        
        58
        +	case l.ch == '{':

      
        
        59
        +		l.advance()

      
        
        60
        +		return Token{LBRACE, "{"}

      
        
        61
        +	case l.ch == '}':

      
        
        62
        +		l.advance()

      
        
        63
        +		return Token{RBRACE, "}"}

      
        
        64
        +	case l.isDigit(), l.ch == '-':

      
        
        65
        +		return l.lexNumber()

      
        
        66
        +	case l.isAlpha():

      
        
        67
        +		offset := l.pos

      
        
        68
        +		for l.isAlpha() {

      
        
        69
        +			l.advance()

      
        
        70
        +		}

      
        
        71
        +		lit := sliceString(l.input, offset, l.pos)

      
        
        72
        +		kind := ILLEGAL

      
        
        73
        +		switch lit {

      
        
        74
        +		case "false", "true":

      
        
        75
        +			kind = BOOL

      
        
        76
        +		case "null":

      
        
        77
        +			kind = NULL

      
        
        78
        +		}

      
        
        79
        +		return Token{kind, lit}

      
        
        80
        +	}

      
        
        81
        +	ch := l.ch

      
        
        82
        +	l.advance()

      
        
        83
        +	return Token{ILLEGAL, string(ch)}

      
        
        84
        +}

      
        
        85
        +

      
        
        86
        +func (l *Lexer) lexString() Token {

      
        
        87
        +	l.advance()

      
        
        88
        +	offset := l.pos

      
        
        89
        +	for {

      
        
        90
        +		switch l.ch {

      
        
        91
        +		default:

      
        
        92
        +			l.advance()

      
        
        93
        +		case 0, '\r', '\n':

      
        
        94
        +			return Token{ILLEGAL, "unterminated string"}

      
        
        95
        +		case '\\':

      
        
        96
        +			l.advance() // consume '\'

      
        
        97
        +			switch l.ch {

      
        
        98
        +			case '"', '\\', '/', 'b', 'f', 'n', 'r', 't':

      
        
        99
        +				l.advance()

      
        
        100
        +			case 'u':

      
        
        101
        +				l.advance()

      
        
        102
        +				for range 4 { // expect exactly 4 hex digits

      
        
        103
        +					if !l.isHex() {

      
        
        104
        +						return Token{ILLEGAL, "invalid unicode escape"}

      
        
        105
        +					}

      
        
        106
        +					l.advance()

      
        
        107
        +				}

      
        
        108
        +			default:

      
        
        109
        +				return Token{ILLEGAL, "invalid escape sequence"}

      
        
        110
        +			}

      
        
        111
        +		case '"':

      
        
        112
        +			lit := sliceString(l.input, offset, l.pos)

      
        
        113
        +			l.advance() // consume closing '"'

      
        
        114
        +			return Token{STRING, lit}

      
        
        115
        +		}

      
        
        116
        +	}

      
        
        117
        +}

      
        
        118
        +

      
        
        119
        +func (l *Lexer) lexNumber() Token {

      
        
        120
        +	offset := l.pos

      
        
        121
        +

      
        
        122
        +	if l.ch == '-' { // optional leading minus

      
        
        123
        +		l.advance()

      
        
        124
        +	}

      
        
        125
        +

      
        
        126
        +	// integer part

      
        
        127
        +	if l.ch == '0' {

      
        
        128
        +		l.advance()

      
        
        129
        +		if l.isDigit() { // leading zero must not be followed by another digit

      
        
        130
        +			return Token{ILLEGAL, "leading zero in number"}

      
        
        131
        +		}

      
        
        132
        +	} else if l.isDigit() {

      
        
        133
        +		for l.isDigit() {

      
        
        134
        +			l.advance()

      
        
        135
        +		}

      
        
        136
        +	} else {

      
        
        137
        +		return Token{ILLEGAL, "invalid number"}

      
        
        138
        +	}

      
        
        139
        +

      
        
        140
        +	kind := NUMBER

      
        
        141
        +	if l.ch == '.' { // optional fractional part

      
        
        142
        +		kind = DECIMAL

      
        
        143
        +		l.advance()

      
        
        144
        +		if !l.isDigit() {

      
        
        145
        +			return Token{ILLEGAL, "expected digit after decimal point"}

      
        
        146
        +		}

      
        
        147
        +		for l.isDigit() {

      
        
        148
        +			l.advance()

      
        
        149
        +		}

      
        
        150
        +	}

      
        
        151
        +

      
        
        152
        +	if l.ch == 'e' || l.ch == 'E' { // optional exponent

      
        
        153
        +		kind = DECIMAL

      
        
        154
        +		l.advance()

      
        
        155
        +		if l.ch == '+' || l.ch == '-' {

      
        
        156
        +			l.advance()

      
        
        157
        +		}

      
        
        158
        +		if !l.isDigit() {

      
        
        159
        +			return Token{ILLEGAL, "expected digit in exponent"}

      
        
        160
        +		}

      
        
        161
        +		for l.isDigit() {

      
        
        162
        +			l.advance()

      
        
        163
        +		}

      
        
        164
        +	}

      
        
        165
        +

      
        
        166
        +	return Token{kind, sliceString(l.input, offset, l.pos)}

      
        
        167
        +}

      
        
        168
        +

      
        
        169
        +func (l *Lexer) lexComment() Token {

      
        
        170
        +	l.advance()

      
        
        171
        +	switch l.ch {

      
        
        172
        +	default:

      
        
        173
        +		return Token{ILLEGAL, "invalid comment"}

      
        
        174
        +	case '/':

      
        
        175
        +		l.advance()

      
        
        176
        +		offset := l.pos

      
        
        177
        +		for l.ch != 0 && l.ch != '\n' && l.ch != '\r' {

      
        
        178
        +			l.advance()

      
        
        179
        +		}

      
        
        180
        +		return Token{COMMENTLINE, sliceString(l.input, offset, l.pos)}

      
        
        181
        +	case '*':

      
        
        182
        +		l.advance()

      
        
        183
        +		offset := l.pos

      
        
        184
        +		for {

      
        
        185
        +			if l.ch == 0 {

      
        
        186
        +				return Token{ILLEGAL, "unterminated block comment"}

      
        
        187
        +			}

      
        
        188
        +			if l.ch == '*' {

      
        
        189
        +				l.advance()

      
        
        190
        +				if l.ch == '/' {

      
        
        191
        +					end := l.pos - 1 // exclude the '*'

      
        
        192
        +					l.advance()

      
        
        193
        +					return Token{COMMENTBLOCK, sliceString(l.input, offset, end)}

      
        
        194
        +				}

      
        
        195
        +			} else {

      
        
        196
        +				l.advance()

      
        
        197
        +			}

      
        
        198
        +		}

      
        
        199
        +	}

      
        
        200
        +}

      
        
        201
        +

      
        
        202
        +func (l *Lexer) advance() {

      
        
        203
        +	if l.rpos >= len(l.input) {

      
        
        204
        +		l.ch = 0

      
        
        205
        +		l.chSize = 0

      
        
        206
        +	} else {

      
        
        207
        +		l.ch, l.chSize = utf8.DecodeRune(l.input[l.rpos:])

      
        
        208
        +	}

      
        
        209
        +	l.pos = l.rpos

      
        
        210
        +	l.rpos += l.chSize

      
        
        211
        +	if l.ch == '\n' || l.ch == '\r' {

      
        
        212
        +		l.line++

      
        
        213
        +		l.col = 0

      
        
        214
        +	} else {

      
        
        215
        +		l.col++

      
        
        216
        +	}

      
        
        217
        +}

      
        
        218
        +func (l *Lexer) isDigit() bool { return l.ch >= '0' && l.ch <= '9' }

      
        
        219
        +func (l *Lexer) isAlpha() bool {

      
        
        220
        +	return (l.ch >= 'a' && l.ch <= 'z') || (l.ch >= 'A' && l.ch <= 'Z')

      
        
        221
        +}

      
        
        222
        +

      
        
        223
        +func (l *Lexer) isHex() bool {

      
        
        224
        +	return (l.ch >= '0' && l.ch <= '9') ||

      
        
        225
        +		(l.ch >= 'a' && l.ch <= 'f') || (l.ch >= 'A' && l.ch <= 'F')

      
        
        226
        +}

      
        
        227
        +

      
        
        228
        +func sliceString(b []byte, start, end int) string {

      
        
        229
        +	if start >= end {

      
        
        230
        +		return ""

      
        
        231
        +	}

      
        
        232
        +	return unsafe.String(&b[start], end-start)

      
        
        233
        +}

      
A lexer_test.go
···
        
        1
        +package json2go

      
        
        2
        +

      
        
        3
        +import "testing"

      
        
        4
        +

      
        
        5
        +func TestLexer_Next(t *testing.T) {

      
        
        6
        +	input := `{"name": "John", "age": 30, "active": true, "score": 3.14, "nothing": null}`

      
        
        7
        +	tests := []struct {

      
        
        8
        +		t TokenType

      
        
        9
        +		l string

      
        
        10
        +	}{

      
        
        11
        +		{LBRACE, "{"},

      
        
        12
        +		{STRING, "name"},

      
        
        13
        +		{COLON, ":"},

      
        
        14
        +		{INDENT, " "},

      
        
        15
        +		{STRING, "John"},

      
        
        16
        +		{COMMA, ","},

      
        
        17
        +		{INDENT, " "},

      
        
        18
        +		{STRING, "age"},

      
        
        19
        +		{COLON, ":"},

      
        
        20
        +		{INDENT, " "},

      
        
        21
        +		{NUMBER, "30"},

      
        
        22
        +		{COMMA, ","},

      
        
        23
        +		{INDENT, " "},

      
        
        24
        +		{STRING, "active"},

      
        
        25
        +		{COLON, ":"},

      
        
        26
        +		{INDENT, " "},

      
        
        27
        +		{BOOL, "true"},

      
        
        28
        +		{COMMA, ","},

      
        
        29
        +		{INDENT, " "},

      
        
        30
        +		{STRING, "score"},

      
        
        31
        +		{COLON, ":"},

      
        
        32
        +		{INDENT, " "},

      
        
        33
        +		{DECIMAL, "3.14"},

      
        
        34
        +		{COMMA, ","},

      
        
        35
        +		{INDENT, " "},

      
        
        36
        +		{STRING, "nothing"},

      
        
        37
        +		{COLON, ":"},

      
        
        38
        +		{INDENT, " "},

      
        
        39
        +		{NULL, "null"},

      
        
        40
        +		{RBRACE, "}"},

      
        
        41
        +		{EOF, ""},

      
        
        42
        +	}

      
        
        43
        +

      
        
        44
        +	l := NewLexer([]byte(input))

      
        
        45
        +	for i, tt := range tests {

      
        
        46
        +		tok := l.Next()

      
        
        47
        +		if tok.Type != tt.t {

      
        
        48
        +			t.Errorf("tests[%d] - wrong token type. expected=%q, got=%q (literal=%q)", i, tt.t, tok.Type, tok.Literal)

      
        
        49
        +		}

      
        
        50
        +		if tok.Literal != tt.l {

      
        
        51
        +			t.Errorf("tests[%d] - wrong literal. expected=%q, got=%q", i, tt.l, tok.Literal)

      
        
        52
        +		}

      
        
        53
        +	}

      
        
        54
        +}

      
        
        55
        +

      
        
        56
        +func TestLexer_comments(t *testing.T) {

      
        
        57
        +	input := `{

      
        
        58
        +		// line comment

      
        
        59
        +		"key": /* block comment */ "value"

      
        
        60
        +	}`

      
        
        61
        +	tests := []struct {

      
        
        62
        +		t TokenType

      
        
        63
        +		l string

      
        
        64
        +	}{

      
        
        65
        +		{LBRACE, "{"},

      
        
        66
        +		{NEWLINE, "\n"},

      
        
        67
        +		{INDENT, "\t\t"},

      
        
        68
        +		{COMMENTLINE, " line comment"},

      
        
        69
        +		{NEWLINE, "\n"},

      
        
        70
        +		{INDENT, "\t\t"},

      
        
        71
        +		{STRING, "key"},

      
        
        72
        +		{COLON, ":"},

      
        
        73
        +		{INDENT, " "},

      
        
        74
        +		{COMMENTBLOCK, " block comment "},

      
        
        75
        +		{INDENT, " "},

      
        
        76
        +		{STRING, "value"},

      
        
        77
        +		{NEWLINE, "\n"},

      
        
        78
        +		{INDENT, "\t"},

      
        
        79
        +		{RBRACE, "}"},

      
        
        80
        +		{EOF, ""},

      
        
        81
        +	}

      
        
        82
        +

      
        
        83
        +	l := NewLexer([]byte(input))

      
        
        84
        +	for i, tt := range tests {

      
        
        85
        +		tok := l.Next()

      
        
        86
        +		if tok.Type != tt.t {

      
        
        87
        +			t.Errorf("tests[%d] - wrong token type. expected=%q, got=%q (literal=%q)", i, tt.t, tok.Type, tok.Literal)

      
        
        88
        +		}

      
        
        89
        +		if tok.Literal != tt.l {

      
        
        90
        +			t.Errorf("tests[%d] - wrong literal. expected=%q, got=%q", i, tt.l, tok.Literal)

      
        
        91
        +		}

      
        
        92
        +	}

      
        
        93
        +}

      
        
        94
        +

      
        
        95
        +func TestLexer_numbers(t *testing.T) {

      
        
        96
        +	input := `[0, -1, 42, 3.14, -0.5, 1e10, 2.5E-3]`

      
        
        97
        +	tests := []struct {

      
        
        98
        +		t TokenType

      
        
        99
        +		l string

      
        
        100
        +	}{

      
        
        101
        +		{LBRACKET, "["},

      
        
        102
        +		{NUMBER, "0"},

      
        
        103
        +		{COMMA, ","},

      
        
        104
        +		{INDENT, " "},

      
        
        105
        +		{NUMBER, "-1"},

      
        
        106
        +		{COMMA, ","},

      
        
        107
        +		{INDENT, " "},

      
        
        108
        +		{NUMBER, "42"},

      
        
        109
        +		{COMMA, ","},

      
        
        110
        +		{INDENT, " "},

      
        
        111
        +		{DECIMAL, "3.14"},

      
        
        112
        +		{COMMA, ","},

      
        
        113
        +		{INDENT, " "},

      
        
        114
        +		{DECIMAL, "-0.5"},

      
        
        115
        +		{COMMA, ","},

      
        
        116
        +		{INDENT, " "},

      
        
        117
        +		{DECIMAL, "1e10"},

      
        
        118
        +		{COMMA, ","},

      
        
        119
        +		{INDENT, " "},

      
        
        120
        +		{DECIMAL, "2.5E-3"},

      
        
        121
        +		{RBRACKET, "]"},

      
        
        122
        +		{EOF, ""},

      
        
        123
        +	}

      
        
        124
        +

      
        
        125
        +	l := NewLexer([]byte(input))

      
        
        126
        +	for i, tt := range tests {

      
        
        127
        +		tok := l.Next()

      
        
        128
        +		if tok.Type != tt.t {

      
        
        129
        +			t.Errorf("tests[%d] - wrong token type. expected=%q, got=%q (literal=%q)", i, tt.t, tok.Type, tok.Literal)

      
        
        130
        +		}

      
        
        131
        +		if tok.Literal != tt.l {

      
        
        132
        +			t.Errorf("tests[%d] - wrong literal. expected=%q, got=%q", i, tt.l, tok.Literal)

      
        
        133
        +		}

      
        
        134
        +	}

      
        
        135
        +}

      
        
        136
        +

      
        
        137
        +func TestLexer_illegal(t *testing.T) {

      
        
        138
        +	tests := []struct{ input, l string }{

      
        
        139
        +		{`"unterminated`, "unterminated string"},

      
        
        140
        +		{`/* unterminated`, "unterminated block comment"},

      
        
        141
        +		{`01`, "leading zero in number"},

      
        
        142
        +		{`-`, "invalid number"},

      
        
        143
        +		{`1.`, "expected digit after decimal point"},

      
        
        144
        +		{`1e`, "expected digit in exponent"},

      
        
        145
        +	}

      
        
        146
        +	for i, tt := range tests {

      
        
        147
        +		l := NewLexer([]byte(tt.input))

      
        
        148
        +		tok := l.Next()

      
        
        149
        +		if tok.Type != ILLEGAL {

      
        
        150
        +			t.Errorf("tests[%d] - expected ILLEGAL, got=%q (literal=%q)", i, tok.Type, tok.Literal)

      
        
        151
        +		}

      
        
        152
        +		if tok.Literal != tt.l {

      
        
        153
        +			t.Errorf("tests[%d] - wrong error literal. expected=%q, got=%q", i, tt.l, tok.Literal)

      
        
        154
        +		}

      
        
        155
        +	}

      
        
        156
        +}

      
A parser.go
···
        
        1
        +package json2go

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"fmt"

      
        
        5
        +	"strconv"

      
        
        6
        +)

      
        
        7
        +

      
        
        8
        +type Parser struct {

      
        
        9
        +	lexer *Lexer

      
        
        10
        +	cur   Token

      
        
        11
        +	peek  Token

      
        
        12
        +}

      
        
        13
        +

      
        
        14
        +func NewParser(l *Lexer) *Parser {

      
        
        15
        +	p := &Parser{lexer: l}

      
        
        16
        +	p.advance() // populate .peek

      
        
        17
        +	p.advance() // populate .cur

      
        
        18
        +	return p

      
        
        19
        +}

      
        
        20
        +

      
        
        21
        +// Parse starts parsing and returns the root Value AST.

      
        
        22
        +// Expects well-formed JSON with a single top-level value.

      
        
        23
        +// Returns an error if the JSON is malformed or has unexpected tokens after the main value.

      
        
        24
        +func (p *Parser) Parse() (Value, error) {

      
        
        25
        +	p.skipNoise()

      
        
        26
        +	v, err := p.parseValue()

      
        
        27
        +	if err != nil {

      
        
        28
        +		return Value{}, err

      
        
        29
        +	}

      
        
        30
        +	p.skipNoise()

      
        
        31
        +	if !p.got(EOF) {

      
        
        32
        +		return Value{}, fmt.Errorf("unexpected token after value: %q", p.cur.Literal)

      
        
        33
        +	}

      
        
        34
        +	return v, nil

      
        
        35
        +}

      
        
        36
        +

      
        
        37
        +func (p *Parser) parseValue() (Value, error) {

      
        
        38
        +	p.skipNoise()

      
        
        39
        +	switch p.cur.Type {

      
        
        40
        +	case LBRACE:

      
        
        41
        +		return p.parseObject()

      
        
        42
        +	case LBRACKET:

      
        
        43
        +		return p.parseArray()

      
        
        44
        +	case STRING:

      
        
        45
        +		v := Value{Kind: StringValue, Str: p.cur.Literal}

      
        
        46
        +		p.advance()

      
        
        47
        +		return v, nil

      
        
        48
        +	case NUMBER:

      
        
        49
        +		n, err := strconv.ParseInt(p.cur.Literal, 10, 64)

      
        
        50
        +		if err != nil {

      
        
        51
        +			f, ferr := strconv.ParseFloat(p.cur.Literal, 64)

      
        
        52
        +			if ferr != nil {

      
        
        53
        +				return Value{}, fmt.Errorf("invalid number: %w", err)

      
        
        54
        +			}

      
        
        55
        +			p.advance()

      
        
        56
        +			return Value{Kind: DecimalValue, Float: f}, nil

      
        
        57
        +		}

      
        
        58
        +		p.advance()

      
        
        59
        +		return Value{Kind: NumberValue, Int: n}, nil

      
        
        60
        +	case DECIMAL:

      
        
        61
        +		f, err := strconv.ParseFloat(p.cur.Literal, 64)

      
        
        62
        +		if err != nil {

      
        
        63
        +			return Value{}, fmt.Errorf("invalid decimal: %w", err)

      
        
        64
        +		}

      
        
        65
        +		p.advance()

      
        
        66
        +		return Value{Kind: DecimalValue, Float: f}, nil

      
        
        67
        +	case BOOL:

      
        
        68
        +		v := Value{Kind: BoolValue, Bool: p.cur.Literal == "true"}

      
        
        69
        +		p.advance()

      
        
        70
        +		return v, nil

      
        
        71
        +	case NULL:

      
        
        72
        +		p.advance()

      
        
        73
        +		return Value{Kind: NullValue}, nil

      
        
        74
        +	default:

      
        
        75
        +		return Value{}, fmt.Errorf("unexpected token %q (%q)", p.cur.Type, p.cur.Literal)

      
        
        76
        +	}

      
        
        77
        +}

      
        
        78
        +

      
        
        79
        +func (p *Parser) parseObject() (Value, error) {

      
        
        80
        +	p.advance()

      
        
        81
        +	var fields []Field

      
        
        82
        +	for {

      
        
        83
        +		p.skipNoise()

      
        
        84
        +		if p.got(RBRACE) {

      
        
        85
        +			p.advance()

      
        
        86
        +			return Value{Kind: ObjectValue, Object: fields}, nil

      
        
        87
        +		}

      
        
        88
        +		if p.got(EOF) {

      
        
        89
        +			return Value{}, fmt.Errorf("unterminated object")

      
        
        90
        +		}

      
        
        91
        +

      
        
        92
        +		keyTok, err := p.expect(STRING)

      
        
        93
        +		if err != nil {

      
        
        94
        +			return Value{}, err

      
        
        95
        +		}

      
        
        96
        +

      
        
        97
        +		p.skipNoise()

      
        
        98
        +		if _, cerr := p.expect(COLON); cerr != nil {

      
        
        99
        +			return Value{}, cerr

      
        
        100
        +		}

      
        
        101
        +

      
        
        102
        +		val, err := p.parseValue()

      
        
        103
        +		if err != nil {

      
        
        104
        +			return Value{}, err

      
        
        105
        +		}

      
        
        106
        +		fields = append(fields, Field{keyTok.Literal, val})

      
        
        107
        +

      
        
        108
        +		p.skipNoise()

      
        
        109
        +		if p.got(COMMA) {

      
        
        110
        +			p.advance()

      
        
        111
        +		} else if !p.got(RBRACE) {

      
        
        112
        +			_, err := p.expect(RBRACE)

      
        
        113
        +			return Value{}, err

      
        
        114
        +		}

      
        
        115
        +	}

      
        
        116
        +}

      
        
        117
        +

      
        
        118
        +func (p *Parser) parseArray() (Value, error) {

      
        
        119
        +	p.advance()

      
        
        120
        +	var items []Value

      
        
        121
        +	for {

      
        
        122
        +		p.skipNoise()

      
        
        123
        +		if p.got(RBRACKET) {

      
        
        124
        +			p.advance()

      
        
        125
        +			return Value{Kind: ArrayValue, Array: items}, nil

      
        
        126
        +		}

      
        
        127
        +		if p.got(EOF) {

      
        
        128
        +			return Value{}, fmt.Errorf("unterminated array")

      
        
        129
        +		}

      
        
        130
        +

      
        
        131
        +		val, err := p.parseValue()

      
        
        132
        +		if err != nil {

      
        
        133
        +			return Value{}, err

      
        
        134
        +		}

      
        
        135
        +		items = append(items, val)

      
        
        136
        +

      
        
        137
        +		p.skipNoise()

      
        
        138
        +		if p.got(COMMA) {

      
        
        139
        +			p.advance()

      
        
        140
        +		} else if !p.got(RBRACKET) {

      
        
        141
        +			_, err := p.expect(RBRACKET)

      
        
        142
        +			return Value{}, err

      
        
        143
        +		}

      
        
        144
        +	}

      
        
        145
        +}

      
        
        146
        +

      
        
        147
        +func (p *Parser) got(kind TokenType) bool { return p.cur.Type == kind }

      
        
        148
        +func (p *Parser) advance() Token {

      
        
        149
        +	prev := p.cur

      
        
        150
        +	p.cur = p.peek

      
        
        151
        +	p.peek = p.lexer.Next()

      
        
        152
        +	return prev

      
        
        153
        +}

      
        
        154
        +

      
        
        155
        +func (p *Parser) expect(kind TokenType) (Token, error) {

      
        
        156
        +	if p.got(kind) {

      
        
        157
        +		return p.advance(), nil

      
        
        158
        +	}

      
        
        159
        +	return p.cur, fmt.Errorf("expected %s, got %s", kind, p.cur.Type)

      
        
        160
        +}

      
        
        161
        +

      
        
        162
        +func (p *Parser) skipNoise() {

      
        
        163
        +	for p.cur.Type == NEWLINE || p.cur.Type == INDENT ||

      
        
        164
        +		p.cur.Type == COMMENTLINE || p.cur.Type == COMMENTBLOCK {

      
        
        165
        +		p.advance()

      
        
        166
        +	}

      
        
        167
        +}

      
A parser_test.go
···
        
        1
        +package json2go

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"encoding/json"

      
        
        5
        +	"reflect"

      
        
        6
        +	"testing"

      
        
        7
        +)

      
        
        8
        +

      
        
        9
        +func TestParser_Parse(t *testing.T) {

      
        
        10
        +	tests := map[string]struct {

      
        
        11
        +		inp      string

      
        
        12
        +		expected Value

      
        
        13
        +		err      bool

      
        
        14
        +	}{

      
        
        15
        +		"string value": {

      
        
        16
        +			inp:      `"hello"`,

      
        
        17
        +			expected: Value{Kind: StringValue, Str: "hello"},

      
        
        18
        +		},

      
        
        19
        +		"integer value":    {inp: `42`, expected: Value{Kind: NumberValue, Int: 42}},

      
        
        20
        +		"negative integer": {inp: `-42`, expected: Value{Kind: NumberValue, Int: -42}},

      
        
        21
        +		"decimal value":    {inp: `3.14`, expected: Value{Kind: DecimalValue, Float: 3.14}},

      
        
        22
        +		"bool true":        {inp: `true`, expected: Value{Kind: BoolValue, Bool: true}},

      
        
        23
        +		"bool false":       {inp: `false`, expected: Value{Kind: BoolValue, Bool: false}},

      
        
        24
        +		"null":             {inp: `null`, expected: Value{Kind: NullValue}},

      
        
        25
        +		"empty object":     {inp: `{}`, expected: Value{Kind: ObjectValue}},

      
        
        26
        +		"empty array":      {inp: `[]`, expected: Value{Kind: ArrayValue}},

      
        
        27
        +		"flat object": {

      
        
        28
        +			inp: `{"name": "John", "age": 30, "active": true}`,

      
        
        29
        +			expected: Value{Kind: ObjectValue, Object: []Field{

      
        
        30
        +				{"name", Value{Kind: StringValue, Str: "John"}},

      
        
        31
        +				{"age", Value{Kind: NumberValue, Int: 30}},

      
        
        32
        +				{"active", Value{Kind: BoolValue, Bool: true}},

      
        
        33
        +			}},

      
        
        34
        +		},

      
        
        35
        +		"nested object": {

      
        
        36
        +			inp: `{"user": {"name": "John", "age": 30}}`,

      
        
        37
        +			expected: Value{Kind: ObjectValue, Object: []Field{

      
        
        38
        +				{"user", Value{Kind: ObjectValue, Object: []Field{

      
        
        39
        +					{"name", Value{Kind: StringValue, Str: "John"}},

      
        
        40
        +					{"age", Value{Kind: NumberValue, Int: 30}},

      
        
        41
        +				}}},

      
        
        42
        +			}},

      
        
        43
        +		},

      
        
        44
        +		"array of numbers": {

      
        
        45
        +			inp: `[1, 2, 3]`,

      
        
        46
        +			expected: Value{Kind: ArrayValue, Array: []Value{

      
        
        47
        +				{Kind: NumberValue, Int: 1},

      
        
        48
        +				{Kind: NumberValue, Int: 2},

      
        
        49
        +				{Kind: NumberValue, Int: 3},

      
        
        50
        +			}},

      
        
        51
        +		},

      
        
        52
        +		"array of objects": {

      
        
        53
        +			inp: `[{"a": 1}, {"a": 2}]`,

      
        
        54
        +			expected: Value{Kind: ArrayValue, Array: []Value{

      
        
        55
        +				{Kind: ObjectValue, Object: []Field{{"a", Value{Kind: NumberValue, Int: 1}}}},

      
        
        56
        +				{Kind: ObjectValue, Object: []Field{{"a", Value{Kind: NumberValue, Int: 2}}}},

      
        
        57
        +			}},

      
        
        58
        +		},

      
        
        59
        +		"object with line comment": {

      
        
        60
        +			inp: `{

      
        
        61
        +				// this is a comment

      
        
        62
        +				"key": "value"

      
        
        63
        +			}`,

      
        
        64
        +			expected: Value{Kind: ObjectValue, Object: []Field{

      
        
        65
        +				{"key", Value{Kind: StringValue, Str: "value"}},

      
        
        66
        +			}},

      
        
        67
        +		},

      
        
        68
        +		"object with block comment": {

      
        
        69
        +			inp: `{"key": /* comment */ "value"}`,

      
        
        70
        +			expected: Value{Kind: ObjectValue, Object: []Field{

      
        
        71
        +				{"key", Value{Kind: StringValue, Str: "value"}},

      
        
        72
        +			}},

      
        
        73
        +		},

      
        
        74
        +		"trailing comma in object": {

      
        
        75
        +			inp: `{"key": "value",}`,

      
        
        76
        +			expected: Value{Kind: ObjectValue, Object: []Field{

      
        
        77
        +				{"key", Value{Kind: StringValue, Str: "value"}},

      
        
        78
        +			}},

      
        
        79
        +		},

      
        
        80
        +		"trailing comma in array": {

      
        
        81
        +			inp: `[1, 2, 3,]`,

      
        
        82
        +			expected: Value{Kind: ArrayValue, Array: []Value{

      
        
        83
        +				{Kind: NumberValue, Int: 1},

      
        
        84
        +				{Kind: NumberValue, Int: 2},

      
        
        85
        +				{Kind: NumberValue, Int: 3},

      
        
        86
        +			}},

      
        
        87
        +		},

      
        
        88
        +		"unterminated object": {inp: `{"key": "value"`, err: true},

      
        
        89
        +		"unterminated array":  {inp: `[1, 2`, err: true},

      
        
        90
        +		"missing colon":       {inp: `{"key" "value"}`, err: true},

      
        
        91
        +		"non-string key":      {inp: `{42: "value"}`, err: true},

      
        
        92
        +		"trailing content":    {inp: `{} 1`, err: true},

      
        
        93
        +	}

      
        
        94
        +	for tname, tt := range tests {

      
        
        95
        +		t.Run(tname, func(t *testing.T) {

      
        
        96
        +			l := NewLexer([]byte(tt.inp))

      
        
        97
        +			p := NewParser(l)

      
        
        98
        +			got, err := p.Parse()

      
        
        99
        +			if tt.err {

      
        
        100
        +				if err == nil {

      
        
        101
        +					t.Errorf("expected error, got nil (value=%+v)", got)

      
        
        102
        +				}

      
        
        103
        +				return

      
        
        104
        +			}

      
        
        105
        +			if err != nil {

      
        
        106
        +				t.Fatalf("unexpected error: %v", err)

      
        
        107
        +			}

      
        
        108
        +			if !reflect.DeepEqual(got, tt.expected) {

      
        
        109
        +				t.Errorf("wrong value\nexpected: %+v\ngot:      %+v", tt.expected, got)

      
        
        110
        +			}

      
        
        111
        +		})

      
        
        112
        +	}

      
        
        113
        +}

      
        
        114
        +

      
        
        115
        +// ensures the parser handles all valid json that encoding/json accepts.

      
        
        116
        +func FuzzParser(f *testing.F) {

      
        
        117
        +	f.Add([]byte("null"))

      
        
        118
        +	f.Add([]byte("true"))

      
        
        119
        +	f.Add([]byte("false"))

      
        
        120
        +	f.Add([]byte("0"))

      
        
        121
        +	f.Add([]byte("1.5"))

      
        
        122
        +	f.Add([]byte("-42"))

      
        
        123
        +	f.Add([]byte("1e10"))

      
        
        124
        +	f.Add([]byte("1.5e-5"))

      
        
        125
        +	f.Add([]byte(`""`))

      
        
        126
        +	f.Add([]byte(`"hello"`))

      
        
        127
        +	f.Add([]byte(`"hello\nworld"`))

      
        
        128
        +	f.Add([]byte(`"hello\\nworld"`))

      
        
        129
        +	f.Add([]byte(`"unicode\u0041"`))

      
        
        130
        +	f.Add([]byte(`[]`))

      
        
        131
        +	f.Add([]byte(`[1,2,3]`))

      
        
        132
        +	f.Add([]byte(`{}`))

      
        
        133
        +	f.Add([]byte(`{"a":1}`))

      
        
        134
        +	f.Add([]byte(`{"a":1,"b":2}`))

      
        
        135
        +	f.Add([]byte(`{"nested":{"x":1}}`))

      
        
        136
        +	f.Add([]byte(`[{"a":1}]`))

      
        
        137
        +	f.Add([]byte(`[1,[2,[3,[4]]]]`))

      
        
        138
        +	f.Add([]byte(`{"a":"b","c":{"d":"e"}}`))

      
        
        139
        +	f.Add([]byte(`[null,true,false]`))

      
        
        140
        +	f.Add([]byte(`{"unicode":"🔥"}`))

      
        
        141
        +	f.Add([]byte(`{ "a" : 1 }`))

      
        
        142
        +	f.Add([]byte("{\n\t\"a\": 1\n}"))

      
        
        143
        +	f.Add([]byte(`{"a":1/*comment*/,"b":2}`))

      
        
        144
        +	f.Add([]byte(`{"a":1//comment

      
        
        145
        +}`))

      
        
        146
        +

      
        
        147
        +	f.Fuzz(func(t *testing.T, data []byte) {

      
        
        148
        +		var any interface{}

      
        
        149
        +		stdErr := json.Unmarshal(data, &any)

      
        
        150
        +

      
        
        151
        +		lexer := NewLexer(data)

      
        
        152
        +		parser := NewParser(lexer)

      
        
        153
        +		_, parseErr := parser.Parse()

      
        
        154
        +

      
        
        155
        +		// If encoding/json accepts it, our parser should too

      
        
        156
        +		if stdErr == nil && parseErr != nil {

      
        
        157
        +			t.Fatalf("encoding/json accepted but our parser rejected: %s\nerror: %v", string(data), parseErr)

      
        
        158
        +		}

      
        
        159
        +

      
        
        160
        +		// If our parser succeeded, transpiler should succeed too

      
        
        161
        +		if parseErr == nil {

      
        
        162
        +			_, transpileErr := Transform("Test", string(data), true)

      
        
        163
        +			if transpileErr != nil {

      
        
        164
        +				t.Fatalf("parser succeeded but transpiler failed: %s\nerror: %v", string(data), transpileErr)

      
        
        165
        +			}

      
        
        166
        +		}

      
        
        167
        +	})

      
        
        168
        +}

      
M readme.txt
···
        1
        1
         json2go

      
        2
        2
         -------

      
        3
        3
         

      
        4
        
        -json2go to go provides a library and cli tool for

      
        5
        
        -converting json strings to go struct definitions

      
        
        4
        +json2go provides a library and cli tool for converting json strings to go struct definitions.

      
        
        5
        +

      
        
        6
        +library Usage:

      
        
        7
        +

      
        
        8
        +    // with json tags

      
        
        9
        +    code, err := json2go.Transform("User", `{"name": "Alice"}`, true)

      
        6
        10
         

      
        7
        
        -    t := json2go. NewTransformer()

      
        8
        
        -    typedef, err := t.Transform(`{"json": true}`, "TypeName")

      
        
        11
        +    // without json tags

      
        
        12
        +    code, err := json2go.Transform("User", `{"name": "Alice"}`, false)

      
        9
        13
         

      
        10
        14
         

      
        11
        15
         cli interface:

      ···
        14
        18
         

      
        15
        19
             echo '{"id": 1, "name": "Alice"}' | json2go

      
        16
        20
             json2go '{"id": 1, "name": "Alice"}'

      
        
        21
        +    json2go --help

      
A testdata/fuzz/FuzzParser/9dc5b3cec0e07f48
···
        
        1
        +go test fuzz v1

      
        
        2
        +[]byte("10000000000000000000")

      
A token.go
···
        
        1
        +package json2go

      
        
        2
        +

      
        
        3
        +type TokenType int

      
        
        4
        +

      
        
        5
        +type Token struct {

      
        
        6
        +	Type    TokenType

      
        
        7
        +	Literal string

      
        
        8
        +}

      
        
        9
        +

      
        
        10
        +//go:generate go tool stringer -output token_type_string.go -type=TokenType

      
        
        11
        +const (

      
        
        12
        +	EOF TokenType = iota

      
        
        13
        +	ILLEGAL

      
        
        14
        +

      
        
        15
        +	NEWLINE // \n

      
        
        16
        +	INDENT  // leading whitespace

      
        
        17
        +

      
        
        18
        +	NUMBER  // 420

      
        
        19
        +	DECIMAL // 69.420

      
        
        20
        +	STRING  // "quote string"

      
        
        21
        +	BOOL    // "true" "false"

      
        
        22
        +	NULL    // "null"

      
        
        23
        +

      
        
        24
        +	COLON    // :

      
        
        25
        +	COMMA    // ,

      
        
        26
        +	LBRACE   // {

      
        
        27
        +	RBRACE   // }

      
        
        28
        +	LBRACKET // [

      
        
        29
        +	RBRACKET // ]

      
        
        30
        +

      
        
        31
        +	COMMENTLINE  // // ...

      
        
        32
        +	COMMENTBLOCK // /* ... */

      
        
        33
        +)

      
A token_type_string.go
···
        
        1
        +// Code generated by "stringer -output token_type_string.go -type=TokenType"; DO NOT EDIT.

      
        
        2
        +

      
        
        3
        +package json2go

      
        
        4
        +

      
        
        5
        +import "strconv"

      
        
        6
        +

      
        
        7
        +func _() {

      
        
        8
        +	// An "invalid array index" compiler error signifies that the constant values have changed.

      
        
        9
        +	// Re-run the stringer command to generate them again.

      
        
        10
        +	var x [1]struct{}

      
        
        11
        +	_ = x[EOF-0]

      
        
        12
        +	_ = x[ILLEGAL-1]

      
        
        13
        +	_ = x[NEWLINE-2]

      
        
        14
        +	_ = x[INDENT-3]

      
        
        15
        +	_ = x[NUMBER-4]

      
        
        16
        +	_ = x[DECIMAL-5]

      
        
        17
        +	_ = x[STRING-6]

      
        
        18
        +	_ = x[BOOL-7]

      
        
        19
        +	_ = x[NULL-8]

      
        
        20
        +	_ = x[COLON-9]

      
        
        21
        +	_ = x[COMMA-10]

      
        
        22
        +	_ = x[LBRACE-11]

      
        
        23
        +	_ = x[RBRACE-12]

      
        
        24
        +	_ = x[LBRACKET-13]

      
        
        25
        +	_ = x[RBRACKET-14]

      
        
        26
        +	_ = x[COMMENTLINE-15]

      
        
        27
        +	_ = x[COMMENTBLOCK-16]

      
        
        28
        +}

      
        
        29
        +

      
        
        30
        +const _TokenType_name = "EOFILLEGALNEWLINEINDENTNUMBERDECIMALSTRINGBOOLNULLCOLONCOMMALBRACERBRACELBRACKETRBRACKETCOMMENTLINECOMMENTBLOCK"

      
        
        31
        +

      
        
        32
        +var _TokenType_index = [...]uint8{0, 3, 10, 17, 23, 29, 36, 42, 46, 50, 55, 60, 66, 72, 80, 88, 99, 111}

      
        
        33
        +

      
        
        34
        +func (i TokenType) String() string {

      
        
        35
        +	idx := int(i) - 0

      
        
        36
        +	if i < 0 || idx >= len(_TokenType_index)-1 {

      
        
        37
        +		return "TokenType(" + strconv.FormatInt(int64(i), 10) + ")"

      
        
        38
        +	}

      
        
        39
        +	return _TokenType_name[_TokenType_index[idx]:_TokenType_index[idx+1]]

      
        
        40
        +}

      
A transpiler.go
···
        
        1
        +package json2go

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"strings"

      
        
        5
        +	"unicode"

      
        
        6
        +)

      
        
        7
        +

      
        
        8
        +// Transpiler transpiles AST [Value] to Go type definitions.

      
        
        9
        +type Transpiler struct{}

      
        
        10
        +

      
        
        11
        +func NewTranspiler() *Transpiler { return &Transpiler{} }

      
        
        12
        +

      
        
        13
        +// Transpile converts a [Value] AST to Go type definitions.

      
        
        14
        +// Nested types are emitted after the parent struct.

      
        
        15
        +func (t *Transpiler) Transpile(structName string, v Value, includeTags bool) (string, error) {

      
        
        16
        +	var buf strings.Builder

      
        
        17
        +	var nested strings.Builder

      
        
        18
        +

      
        
        19
        +	switch v.Kind {

      
        
        20
        +	case ArrayValue:

      
        
        21
        +		if len(v.Array) == 0 {

      
        
        22
        +			buf.WriteString("type ")

      
        
        23
        +			buf.WriteString(structName)

      
        
        24
        +			buf.WriteString(" []any")

      
        
        25
        +		} else {

      
        
        26
        +			itemType := t.writeWithNested(&buf, &nested, structName+"Item", v.Array[0], includeTags)

      
        
        27
        +			buf.WriteString("type ")

      
        
        28
        +			buf.WriteString(structName)

      
        
        29
        +			buf.WriteString(" []")

      
        
        30
        +			buf.WriteString(itemType)

      
        
        31
        +		}

      
        
        32
        +

      
        
        33
        +	case ObjectValue:

      
        
        34
        +		t.writeStructDef(&buf, &nested, structName, v.Object, includeTags)

      
        
        35
        +

      
        
        36
        +	default:

      
        
        37
        +		scalarType := t.inferType(structName, v)

      
        
        38
        +		buf.WriteString("type ")

      
        
        39
        +		buf.WriteString(structName)

      
        
        40
        +		buf.WriteByte(' ')

      
        
        41
        +		buf.WriteString(scalarType)

      
        
        42
        +	}

      
        
        43
        +

      
        
        44
        +	if nested.Len() > 0 && buf.Len() > 0 {

      
        
        45
        +		buf.WriteString("\n\n")

      
        
        46
        +		buf.WriteString(nested.String())

      
        
        47
        +	}

      
        
        48
        +	return buf.String(), nil

      
        
        49
        +}

      
        
        50
        +

      
        
        51
        +func (t *Transpiler) writeWithNested(buf, nested *strings.Builder, name string, v Value, includeTags bool) string {

      
        
        52
        +	switch v.Kind {

      
        
        53
        +	case ObjectValue:

      
        
        54
        +		t.writeStructDef(nested, nested, name, v.Object, includeTags)

      
        
        55
        +		return name

      
        
        56
        +

      
        
        57
        +	case ArrayValue:

      
        
        58
        +		if len(v.Array) == 0 {

      
        
        59
        +			return "[]any"

      
        
        60
        +		}

      
        
        61
        +		itemType := t.writeWithNested(buf, nested, name+"Item", v.Array[0], includeTags)

      
        
        62
        +		return "[]" + itemType

      
        
        63
        +	}

      
        
        64
        +	return t.inferType(name, v)

      
        
        65
        +}

      
        
        66
        +

      
        
        67
        +func (t *Transpiler) writeStructDef(buf, nested *strings.Builder, name string, fields []Field, includeTags bool) {

      
        
        68
        +	if buf.Len() > 0 {

      
        
        69
        +		buf.WriteString("\n\n")

      
        
        70
        +	}

      
        
        71
        +

      
        
        72
        +	buf.WriteString("type ")

      
        
        73
        +	buf.WriteString(name)

      
        
        74
        +	buf.WriteString(" struct {\n")

      
        
        75
        +	for _, f := range fields {

      
        
        76
        +		fieldName := t.sanitizeFieldName(f.K)

      
        
        77
        +		fieldType := t.writeWithNested(buf, nested, name+fieldName, f.V, includeTags)

      
        
        78
        +		if includeTags {

      
        
        79
        +			buf.WriteByte('\t')

      
        
        80
        +			buf.WriteString(fieldName)

      
        
        81
        +			buf.WriteByte(' ')

      
        
        82
        +			buf.WriteString(fieldType)

      
        
        83
        +			buf.WriteString(" `json:\"")

      
        
        84
        +			buf.WriteString(f.K)

      
        
        85
        +			buf.WriteString("\"`\n")

      
        
        86
        +		} else {

      
        
        87
        +			buf.WriteByte('\t')

      
        
        88
        +			buf.WriteString(fieldName)

      
        
        89
        +			buf.WriteByte(' ')

      
        
        90
        +			buf.WriteString(fieldType)

      
        
        91
        +			buf.WriteByte('\n')

      
        
        92
        +		}

      
        
        93
        +	}

      
        
        94
        +	buf.WriteString("}")

      
        
        95
        +}

      
        
        96
        +

      
        
        97
        +func (t *Transpiler) inferType(name string, v Value) string {

      
        
        98
        +	switch v.Kind {

      
        
        99
        +	case ObjectValue:

      
        
        100
        +		return name

      
        
        101
        +	case ArrayValue:

      
        
        102
        +		if len(v.Array) == 0 {

      
        
        103
        +			return "[]any"

      
        
        104
        +		}

      
        
        105
        +		itemType := t.inferType(name+"Item", v.Array[0])

      
        
        106
        +		return "[]" + itemType

      
        
        107
        +	case StringValue:

      
        
        108
        +		return "string"

      
        
        109
        +	case NumberValue:

      
        
        110
        +		return "int"

      
        
        111
        +	case DecimalValue:

      
        
        112
        +		return "float64"

      
        
        113
        +	case BoolValue:

      
        
        114
        +		return "bool"

      
        
        115
        +	case NullValue:

      
        
        116
        +		return "any"

      
        
        117
        +	default:

      
        
        118
        +		return "any"

      
        
        119
        +	}

      
        
        120
        +}

      
        
        121
        +

      
        
        122
        +func (t *Transpiler) sanitizeFieldName(jsonKey string) string {

      
        
        123
        +	if jsonKey == "" {

      
        
        124
        +		return "Field"

      
        
        125
        +	}

      
        
        126
        +

      
        
        127
        +	var result strings.Builder

      
        
        128
        +	result.Grow(len(jsonKey))

      
        
        129
        +

      
        
        130
        +	capitalize := true

      
        
        131
        +	for _, r := range jsonKey {

      
        
        132
        +		if r == '_' {

      
        
        133
        +			capitalize = true

      
        
        134
        +			continue

      
        
        135
        +		}

      
        
        136
        +		if capitalize {

      
        
        137
        +			result.WriteRune(unicode.ToUpper(r))

      
        
        138
        +			capitalize = false

      
        
        139
        +		} else {

      
        
        140
        +			result.WriteRune(r)

      
        
        141
        +		}

      
        
        142
        +	}

      
        
        143
        +

      
        
        144
        +	name := result.String()

      
        
        145
        +	if name != "" && isValidIdentifier(name) {

      
        
        146
        +		return name

      
        
        147
        +	}

      
        
        148
        +

      
        
        149
        +	return t.sanitizeInvalidIdentifier(jsonKey)

      
        
        150
        +}

      
        
        151
        +

      
        
        152
        +func (t *Transpiler) sanitizeInvalidIdentifier(jsonKey string) string {

      
        
        153
        +	var result strings.Builder

      
        
        154
        +	for i, r := range jsonKey {

      
        
        155
        +		if unicode.IsLetter(r) || r == '_' || (i > 0 && unicode.IsDigit(r)) {

      
        
        156
        +			result.WriteRune(r)

      
        
        157
        +		} else if i > 0 && r == '-' {

      
        
        158
        +			result.WriteRune('_')

      
        
        159
        +		}

      
        
        160
        +	}

      
        
        161
        +

      
        
        162
        +	name := result.String()

      
        
        163
        +	if name == "" || (!unicode.IsLetter(rune(name[0])) && name[0] != '_') {

      
        
        164
        +		var b strings.Builder

      
        
        165
        +		b.Grow(len(name) + 1)

      
        
        166
        +		b.WriteByte('F')

      
        
        167
        +		b.WriteString(name)

      
        
        168
        +		return b.String()

      
        
        169
        +	}

      
        
        170
        +

      
        
        171
        +	return name

      
        
        172
        +}

      
A transpiler_test.go
···
        
        1
        +package json2go

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"strings"

      
        
        5
        +	"testing"

      
        
        6
        +)

      
        
        7
        +

      
        
        8
        +func TestTranspiler_Transpile_WithTags(t *testing.T) {

      
        
        9
        +	tests := map[string]struct {

      
        
        10
        +		v     Value

      
        
        11
        +		name  string

      
        
        12
        +		check func(t *testing.T, result string)

      
        
        13
        +	}{

      
        
        14
        +		"simple object": {

      
        
        15
        +			name: "User",

      
        
        16
        +			v: Value{

      
        
        17
        +				Kind: ObjectValue,

      
        
        18
        +				Object: []Field{

      
        
        19
        +					{K: "name", V: Value{Kind: StringValue, Str: "John"}},

      
        
        20
        +					{K: "age", V: Value{Kind: NumberValue, Int: 30}},

      
        
        21
        +				},

      
        
        22
        +			},

      
        
        23
        +			check: func(t *testing.T, result string) {

      
        
        24
        +				if !strings.Contains(result, "type User struct") {

      
        
        25
        +					t.Errorf("missing User struct")

      
        
        26
        +				}

      
        
        27
        +				if !strings.Contains(result, "Name string `json:\"name\"`") {

      
        
        28
        +					t.Errorf("missing Name field")

      
        
        29
        +				}

      
        
        30
        +				if !strings.Contains(result, "Age int `json:\"age\"`") {

      
        
        31
        +					t.Errorf("missing Age field")

      
        
        32
        +				}

      
        
        33
        +			},

      
        
        34
        +		},

      
        
        35
        +		"nested object": {

      
        
        36
        +			name: "Response",

      
        
        37
        +			v: Value{

      
        
        38
        +				Kind: ObjectValue,

      
        
        39
        +				Object: []Field{{

      
        
        40
        +					K: "user",

      
        
        41
        +					V: Value{

      
        
        42
        +						Kind: ObjectValue,

      
        
        43
        +						Object: []Field{

      
        
        44
        +							{K: "name", V: Value{Kind: StringValue, Str: "Alice"}},

      
        
        45
        +							{K: "active", V: Value{Kind: BoolValue, Bool: true}},

      
        
        46
        +						},

      
        
        47
        +					},

      
        
        48
        +				}},

      
        
        49
        +			},

      
        
        50
        +			check: func(t *testing.T, result string) {

      
        
        51
        +				if !strings.Contains(result, "type Response struct") {

      
        
        52
        +					t.Errorf("missing Response struct")

      
        
        53
        +				}

      
        
        54
        +				if !strings.Contains(result, "type ResponseUser struct") {

      
        
        55
        +					t.Errorf("missing ResponseUser struct")

      
        
        56
        +				}

      
        
        57
        +				if !strings.Contains(result, "User ResponseUser `json:\"user\"`") {

      
        
        58
        +					t.Errorf("missing User field")

      
        
        59
        +				}

      
        
        60
        +			},

      
        
        61
        +		},

      
        
        62
        +		"array of scalars": {

      
        
        63
        +			name: "Tags",

      
        
        64
        +			v: Value{

      
        
        65
        +				Kind:  ArrayValue,

      
        
        66
        +				Array: []Value{{Kind: StringValue, Str: "go"}},

      
        
        67
        +			},

      
        
        68
        +			check: func(t *testing.T, result string) {

      
        
        69
        +				if result != "type Tags []string" {

      
        
        70
        +					t.Errorf("expected scalar array type, got: %s", result)

      
        
        71
        +				}

      
        
        72
        +			},

      
        
        73
        +		},

      
        
        74
        +		"array of objects": {

      
        
        75
        +			name: "Users",

      
        
        76
        +			v: Value{

      
        
        77
        +				Kind: ArrayValue,

      
        
        78
        +				Array: []Value{{

      
        
        79
        +					Kind: ObjectValue,

      
        
        80
        +					Object: []Field{

      
        
        81
        +						{K: "id", V: Value{Kind: NumberValue, Int: 1}},

      
        
        82
        +						{K: "name", V: Value{Kind: StringValue, Str: "Bob"}},

      
        
        83
        +					},

      
        
        84
        +				}},

      
        
        85
        +			},

      
        
        86
        +			check: func(t *testing.T, result string) {

      
        
        87
        +				if !strings.Contains(result, "type UsersItem struct") {

      
        
        88
        +					t.Errorf("missing UsersItem struct in: %s", result)

      
        
        89
        +				}

      
        
        90
        +				if !strings.Contains(result, "Id int `json:\"id\"`") {

      
        
        91
        +					t.Errorf("missing Id field")

      
        
        92
        +				}

      
        
        93
        +			},

      
        
        94
        +		},

      
        
        95
        +		"snake_case fields": {

      
        
        96
        +			name: "Data",

      
        
        97
        +			v: Value{

      
        
        98
        +				Kind: ObjectValue,

      
        
        99
        +				Object: []Field{

      
        
        100
        +					{K: "first_name", V: Value{Kind: StringValue, Str: "Jane"}},

      
        
        101
        +					{K: "last_name", V: Value{Kind: StringValue, Str: "Doe"}},

      
        
        102
        +				},

      
        
        103
        +			},

      
        
        104
        +			check: func(t *testing.T, result string) {

      
        
        105
        +				if !strings.Contains(result, "FirstName string `json:\"first_name\"`") {

      
        
        106
        +					t.Errorf("missing FirstName field")

      
        
        107
        +				}

      
        
        108
        +				if !strings.Contains(result, "LastName string `json:\"last_name\"`") {

      
        
        109
        +					t.Errorf("missing LastName field")

      
        
        110
        +				}

      
        
        111
        +			},

      
        
        112
        +		},

      
        
        113
        +	}

      
        
        114
        +	for tname, tt := range tests {

      
        
        115
        +		t.Run(tname, func(t *testing.T) {

      
        
        116
        +			result, err := NewTranspiler().Transpile(tt.name, tt.v, true)

      
        
        117
        +			if err != nil {

      
        
        118
        +				t.Fatalf("unexpected error: %v", err)

      
        
        119
        +			}

      
        
        120
        +			tt.check(t, result)

      
        
        121
        +		})

      
        
        122
        +	}

      
        
        123
        +}

      
        
        124
        +

      
        
        125
        +func TestTranspiler_Transpile_WithoutTags(t *testing.T) {

      
        
        126
        +	tests := map[string]struct {

      
        
        127
        +		v     Value

      
        
        128
        +		name  string

      
        
        129
        +		check func(t *testing.T, result string)

      
        
        130
        +	}{

      
        
        131
        +		"simple object": {

      
        
        132
        +			name: "User",

      
        
        133
        +			v: Value{

      
        
        134
        +				Kind: ObjectValue,

      
        
        135
        +				Object: []Field{

      
        
        136
        +					{K: "name", V: Value{Kind: StringValue, Str: "John"}},

      
        
        137
        +					{K: "age", V: Value{Kind: NumberValue, Int: 30}},

      
        
        138
        +				},

      
        
        139
        +			},

      
        
        140
        +			check: func(t *testing.T, result string) {

      
        
        141
        +				if !strings.Contains(result, "type User struct") {

      
        
        142
        +					t.Errorf("missing User struct")

      
        
        143
        +				}

      
        
        144
        +				if !strings.Contains(result, "Name string\n") {

      
        
        145
        +					t.Errorf("missing Name field without tag")

      
        
        146
        +				}

      
        
        147
        +				if !strings.Contains(result, "Age int\n") {

      
        
        148
        +					t.Errorf("missing Age field without tag")

      
        
        149
        +				}

      
        
        150
        +				if strings.Contains(result, "`json:") {

      
        
        151
        +					t.Errorf("should not have json tags")

      
        
        152
        +				}

      
        
        153
        +			},

      
        
        154
        +		},

      
        
        155
        +		"nested object": {

      
        
        156
        +			name: "Response",

      
        
        157
        +			v: Value{

      
        
        158
        +				Kind: ObjectValue,

      
        
        159
        +				Object: []Field{{

      
        
        160
        +					K: "user",

      
        
        161
        +					V: Value{

      
        
        162
        +						Kind: ObjectValue,

      
        
        163
        +						Object: []Field{

      
        
        164
        +							{K: "name", V: Value{Kind: StringValue, Str: "Alice"}},

      
        
        165
        +							{K: "active", V: Value{Kind: BoolValue, Bool: true}},

      
        
        166
        +						},

      
        
        167
        +					},

      
        
        168
        +				}},

      
        
        169
        +			},

      
        
        170
        +			check: func(t *testing.T, result string) {

      
        
        171
        +				if !strings.Contains(result, "type Response struct") {

      
        
        172
        +					t.Errorf("missing Response struct")

      
        
        173
        +				}

      
        
        174
        +				if !strings.Contains(result, "type ResponseUser struct") {

      
        
        175
        +					t.Errorf("missing ResponseUser struct")

      
        
        176
        +				}

      
        
        177
        +				if strings.Contains(result, "`json:") {

      
        
        178
        +					t.Errorf("should not have json tags")

      
        
        179
        +				}

      
        
        180
        +			},

      
        
        181
        +		},

      
        
        182
        +		"snake_case fields preserved": {

      
        
        183
        +			name: "Data",

      
        
        184
        +			v: Value{

      
        
        185
        +				Kind: ObjectValue,

      
        
        186
        +				Object: []Field{

      
        
        187
        +					{K: "first_name", V: Value{Kind: StringValue, Str: "Jane"}},

      
        
        188
        +					{K: "last_name", V: Value{Kind: StringValue, Str: "Doe"}},

      
        
        189
        +				},

      
        
        190
        +			},

      
        
        191
        +			check: func(t *testing.T, result string) {

      
        
        192
        +				if !strings.Contains(result, "FirstName string\n") {

      
        
        193
        +					t.Errorf("missing FirstName field")

      
        
        194
        +				}

      
        
        195
        +				if !strings.Contains(result, "LastName string\n") {

      
        
        196
        +					t.Errorf("missing LastName field")

      
        
        197
        +				}

      
        
        198
        +			},

      
        
        199
        +		},

      
        
        200
        +	}

      
        
        201
        +	for tname, tt := range tests {

      
        
        202
        +		t.Run(tname, func(t *testing.T) {

      
        
        203
        +			result, err := NewTranspiler().Transpile(tt.name, tt.v, false)

      
        
        204
        +			if err != nil {

      
        
        205
        +				t.Fatalf("unexpected error: %v", err)

      
        
        206
        +			}

      
        
        207
        +			tt.check(t, result)

      
        
        208
        +		})

      
        
        209
        +	}

      
        
        210
        +}

      
        
        211
        +

      
        
        212
        +func TestTranspiler_ParserIntegration(t *testing.T) {

      
        
        213
        +	tests := map[string]struct {

      
        
        214
        +		input       string

      
        
        215
        +		structName  string

      
        
        216
        +		includeTags bool

      
        
        217
        +		expectTags  bool

      
        
        218
        +	}{

      
        
        219
        +		"parse and transpile with tags": {

      
        
        220
        +			input:       `{"user_name": "alice", "user_age": 25}`,

      
        
        221
        +			structName:  "Profile",

      
        
        222
        +			includeTags: true,

      
        
        223
        +			expectTags:  true,

      
        
        224
        +		},

      
        
        225
        +		"parse and transpile without tags": {

      
        
        226
        +			input:       `{"user_name": "bob", "user_age": 30}`,

      
        
        227
        +			structName:  "Account",

      
        
        228
        +			includeTags: false,

      
        
        229
        +			expectTags:  false,

      
        
        230
        +		},

      
        
        231
        +	}

      
        
        232
        +	for tname, tt := range tests {

      
        
        233
        +		t.Run(tname, func(t *testing.T) {

      
        
        234
        +			lexer := NewLexer([]byte(tt.input))

      
        
        235
        +			parser := NewParser(lexer)

      
        
        236
        +			v, err := parser.Parse()

      
        
        237
        +			if err != nil {

      
        
        238
        +				t.Fatalf("parse error: %v", err)

      
        
        239
        +			}

      
        
        240
        +

      
        
        241
        +			result, err := NewTranspiler().Transpile(tt.structName, v, tt.includeTags)

      
        
        242
        +			if err != nil {

      
        
        243
        +				t.Fatalf("transpile error: %v", err)

      
        
        244
        +			}

      
        
        245
        +

      
        
        246
        +			hasTag := strings.Contains(result, "`json:")

      
        
        247
        +			if tt.expectTags != hasTag {

      
        
        248
        +				t.Errorf("expected tags=%v, got=%v\n%s", tt.expectTags, hasTag, result)

      
        
        249
        +			}

      
        
        250
        +

      
        
        251
        +			if !strings.Contains(result, "type "+tt.structName) {

      
        
        252
        +				t.Errorf("struct name %q not found in output", tt.structName)

      
        
        253
        +			}

      
        
        254
        +		})

      
        
        255
        +	}

      
        
        256
        +}

      
A value.go
···
        
        1
        +package json2go

      
        
        2
        +

      
        
        3
        +// ValueType the kind of json value represented by a [Value] node.

      
        
        4
        +type ValueType int

      
        
        5
        +

      
        
        6
        +const (

      
        
        7
        +	NullValue ValueType = iota

      
        
        8
        +	BoolValue

      
        
        9
        +	StringValue

      
        
        10
        +	NumberValue

      
        
        11
        +	DecimalValue

      
        
        12
        +	ObjectValue

      
        
        13
        +	ArrayValue

      
        
        14
        +)

      
        
        15
        +

      
        
        16
        +// Value represents a json value in the AST.

      
        
        17
        +type Value struct {

      
        
        18
        +	Kind ValueType

      
        
        19
        +

      
        
        20
        +	// only one of these is set depending on Kind

      
        
        21
        +	Str    string

      
        
        22
        +	Int    int64

      
        
        23
        +	Float  float64

      
        
        24
        +	Bool   bool

      
        
        25
        +	Object []Field // ordered, preserves key order

      
        
        26
        +	Array  []Value

      
        
        27
        +}

      
        
        28
        +

      
        
        29
        +type Field struct {

      
        
        30
        +	K string

      
        
        31
        +	V Value

      
        
        32
        +}