2 files changed,
63 insertions(+),
94 deletions(-)
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed at:
2025-11-26 20:02:26 +0200
Authored at:
2025-11-26 15:06:51 +0200
Change ID:
ynmyovkztxlmmpzpnxmzvxlrwrukuyvn
Parent:
af83030
jump to
| M | json2go.go |
| M | json2go_test.go |
M
json2go.go
··· 4 4 "encoding/json" 5 5 "errors" 6 6 "fmt" 7 + "sort" 7 8 "strings" 8 9 ) 9 10 10 11 var ErrInvalidJSON = errors.New("invalid json") 11 12 12 -type ( 13 - types struct{ name, def string } 14 - Transformer struct { 15 - structName string 16 - types []types 17 - } 18 -) 13 +type Transformer struct { 14 + structName string 15 +} 19 16 20 17 func NewTransformer() *Transformer { 21 18 return &Transformer{} ··· 27 24 // todo: validate provided structName 28 25 func (t *Transformer) Transform(structName, jsonStr string) (string, error) { 29 26 t.structName = structName 30 - t.types = make([]types, 1) 31 27 32 28 var input any 33 29 if err := json.Unmarshal([]byte(jsonStr), &input); err != nil { 34 30 return "", errors.Join(ErrInvalidJSON, err) 35 31 } 36 32 37 - var result strings.Builder 38 - 39 - // the "parent" type 40 - type_ := t.generateTypeAnnotation(structName, input) 41 - result.WriteString(type_) 42 - 43 - // nested types 44 - for _, t := range t.types { 45 - if t.name != structName { 46 - result.WriteString(t.def) 47 - result.WriteString("\n") 48 - } 49 - } 50 - 51 - return result.String(), nil 33 + type_ := t.getTypeAnnotation(structName, input) 34 + return type_, nil 52 35 } 53 36 54 -func (t *Transformer) generateTypeAnnotation(typeName string, input any) string { 37 +func (t *Transformer) getTypeAnnotation(typeName string, input any) string { 55 38 switch v := input.(type) { 56 39 case map[string]any: 57 - return t.buildStruct(typeName, v) 40 + return fmt.Sprintf("type %s %s", typeName, t.buildStruct(v)) 58 41 59 42 case []any: 60 43 if len(v) == 0 { ··· 75 58 76 59 case bool: 77 60 return fmt.Sprintf("type %s bool", typeName) 78 - 79 - case nil: 80 - return fmt.Sprintf("type %s any", typeName) 81 61 82 62 default: 83 63 return fmt.Sprintf("type %s any", typeName) ··· 86 66 } 87 67 88 68 // todo: input shouldn't be map, to preserve it's order 89 -func (t *Transformer) buildStruct(typeName string, input map[string]any) string { 69 +func (t *Transformer) buildStruct(input map[string]any) string { 90 70 var fields strings.Builder 91 - for key, value := range input { 92 - fieldName := t.toGoFieldName(key) 71 + for _, f := range mapToStructInput(input) { 72 + fieldName := t.toGoFieldName(f.field) 93 73 if fieldName == "" { 94 74 fieldName = "Field" 95 75 } 96 76 97 - fieldType := t.getGoType(fieldName, value) 77 + fieldType := t.getGoType(fieldName, f.type_) 98 78 99 79 // todo: toggle json tags generation 100 - jsonTag := fmt.Sprintf("`json:\"%s\"`", key) 80 + jsonTag := fmt.Sprintf("`json:\"%s\"`", f.field) 101 81 102 82 // todo: figure out the indentation, since it might have nested struct 103 83 fields.WriteString(fmt.Sprintf( ··· 108 88 )) 109 89 } 110 90 111 - structDef := fmt.Sprintf("type %s struct {\n%s}", typeName, fields.String()) 112 - t.types = append(t.types, types{ 113 - name: typeName, 114 - def: structDef, 115 - }) 116 - 117 - return structDef 91 + return fmt.Sprintf("struct {\n%s}", fields.String()) 118 92 } 119 93 120 94 func (t *Transformer) getGoType(fieldName string, value any) string { 121 95 switch v := value.(type) { 122 96 case map[string]any: 123 - typeName := t.toGoTypeName(fieldName) 124 - if !t.isTypeRecorded(typeName) { 125 - t.generateTypeAnnotation(typeName, v) 126 - } 127 - return typeName 97 + return t.buildStruct(v) 128 98 129 99 case []any: 130 100 if len(v) == 0 { 131 101 return "[]any" 132 102 } 133 103 134 - type_ := t.getGoType(fieldName+"Item", v[0]) // TODO 104 + type_ := t.getGoType(fieldName, v[0]) 135 105 return "[]" + type_ 136 106 137 107 case float64: ··· 145 115 146 116 case bool: 147 117 return "bool" 148 - 149 - case nil: 150 - return "any" 151 118 152 119 default: 153 120 return "any" 154 121 } 155 122 } 156 123 157 -func (t *Transformer) toGoTypeName(fieldName string) string { 158 - goName := t.toGoFieldName(fieldName) 159 - if len(goName) > 0 { 160 - return strings.ToUpper(goName[:1]) + goName[1:] 161 - } 162 - return "Type" 163 -} 164 - 165 124 func (t *Transformer) toGoFieldName(jsonField string) string { 166 125 parts := strings.Split(jsonField, "_") 167 126 ··· 177 136 return result.String() 178 137 } 179 138 180 -func (t *Transformer) isTypeRecorded(name string) bool { 181 - for _, t := range t.types { 182 - if t.name == name { 183 - return true 184 - } 139 +type structInput struct { 140 + field string 141 + type_ any 142 +} 143 + 144 +func mapToStructInput(input map[string]any) []structInput { 145 + res := make([]structInput, 0, len(input)) 146 + for k, v := range input { 147 + res = append(res, structInput{k, v}) 185 148 } 186 - return false 149 + 150 + sort.Slice(res, func(i, j int) bool { 151 + return res[i].field < res[j].field 152 + }) 153 + 154 + return res 187 155 }
M
json2go_test.go
··· 3 3 import ( 4 4 "errors" 5 5 "fmt" 6 + "reflect" 6 7 "strings" 7 8 "testing" 8 9 ) 9 10 10 11 func field(name, type_ string, json_ ...string) string { 12 + if strings.Contains(type_, "struct") { 13 + return fmt.Sprintf("\n%s %s", name, type_) 14 + } 15 + 11 16 tag := strings.ToLower(name) 12 17 if len(json_) == 1 { 13 18 tag = json_[0] ··· 24 29 "simple object": { 25 30 input: `{"name": "Olex", "active": true, "age": 420}`, 26 31 output: "type Out struct {" + 27 - field("Name", "string") + 28 32 field("Active", "bool") + 29 33 field("Age", "int") + 30 - "\n}\n", 34 + field("Name", "string") + 35 + "\n}", 31 36 }, 32 37 "invalid json": { 33 38 err: ErrInvalidJSON, ··· 38 43 output: "type Out struct {" + 39 44 field("FirstName", "string", "first_name") + 40 45 field("LastName", "string", "last_name") + 41 - "\n}\n", 46 + "\n}", 42 47 }, 43 48 "nested object and array": { 44 49 input: `{"user": {"name": "Alice", "score": 95.5}, "tags": ["go", "json"]}`, 45 50 output: "type Out struct {" + 46 - field("User", "User") + 47 51 field("Tags", "[]string") + 48 - "\n}\ntype User struct {" + 52 + field("User", "struct {") + 49 53 field("Name", "string") + 50 54 field("Score", "float64") + 51 - "\n}\n", 55 + "\n} `json:\"user\"`" + 56 + "\n}", 52 57 }, 53 58 "empty nested object": { 54 59 input: `{"user": {}}`, 55 60 output: "type Out struct {" + 56 - field("User", "User") + 57 - "\n}\ntype User struct {\n}\n", 61 + field("User", "struct {") + 62 + "\n} `json:\"user\"`" + 63 + "\n}", 58 64 }, 59 65 "array of object": { 60 66 input: `[{"name": "John"}, {"name": "Jane"}]`, 61 - output: "type Out []OutItem" + 62 - "\ntype OutItem struct {" + 67 + output: "type Out []struct {" + 63 68 field("Name", "string") + 64 - "\n}\n", 69 + "\n}", 65 70 }, 66 71 "empty array": { 67 72 input: `{"items": []}`, 68 - output: `type Out struct {` + 69 - field("Items", "[]any") + "\n}\n", 73 + output: "type Out struct {" + 74 + field("Items", "[]any") + 75 + "\n}", 70 76 }, 71 77 "null": { 72 78 input: `{"item": null}`, 73 79 output: `type Out struct {` + 74 - field("Item", "any") + "\n}\n", 80 + field("Item", "any") + 81 + "\n}", 75 82 }, 76 83 "numbers": { 77 84 input: `{"pos": 123, "neg": -321, "float": 420.69}`, 78 85 output: "type Out struct {" + 79 - field("Pos", "int") + 80 - field("Neg", "int") + 81 86 field("Float", "float64") + 82 - "\n}\n", 87 + field("Neg", "int") + 88 + field("Pos", "int") + 89 + "\n}", 83 90 }, 84 91 } 85 92 ··· 88 95 t.Run(tname, func(t *testing.T) { 89 96 result, err := trans.Transform("Out", tt.input) 90 97 assertEqualErr(t, tt.err, err) 91 - 92 - lines := strings.Split(result, "\n") 93 - counts := make(map[string]int) 94 - for _, line := range lines { 95 - if !strings.Contains(line, "}") { 96 - counts[line]++ 97 - } 98 - } 99 - 100 - for _, line := range lines { 101 - if counts[line] > 1 { 102 - t.Fatalf("found duplicate line: %s", line) 103 - } 104 - } 98 + assertEqual(t, tt.output, result) 105 99 }) 106 100 } 107 101 } ··· 112 106 t.Errorf("expected: %v, got: %v\n", expected, actual) 113 107 } 114 108 } 109 + 110 +func assertEqual[T any](t *testing.T, expected, actual T) { 111 + t.Helper() 112 + if !reflect.DeepEqual(expected, actual) { 113 + t.Errorf("expected: %v, got: %v\n", expected, actual) 114 + } 115 +}