19 files changed,
1508 insertions(+),
383 deletions(-)
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed by:
GitHub
noreply@github.com
Committed at:
2026-05-26 14:07:11 +0300
Parent:
34a739e
jump to
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 }
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 +}