2 files changed,
63 insertions(+),
94 deletions(-)
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed at:
2025-11-26 20:02:26 +0200
Change ID:
ynmyovkztxlmmpzpnxmzvxlrwrukuyvn
Parent:
af83030
jump to
| M | json2go.go |
| M | json2go_test.go |
M
json2go.go
@@ -4,18 +4,15 @@ import (
"encoding/json" "errors" "fmt" + "sort" "strings" ) var ErrInvalidJSON = errors.New("invalid json") -type ( - types struct{ name, def string } - Transformer struct { - structName string - types []types - } -) +type Transformer struct { + structName string +} func NewTransformer() *Transformer { return &Transformer{}@@ -27,34 +24,20 @@ // todo: output as io.Writer?
// todo: validate provided structName func (t *Transformer) Transform(structName, jsonStr string) (string, error) { t.structName = structName - t.types = make([]types, 1) var input any if err := json.Unmarshal([]byte(jsonStr), &input); err != nil { return "", errors.Join(ErrInvalidJSON, err) } - var result strings.Builder - - // the "parent" type - type_ := t.generateTypeAnnotation(structName, input) - result.WriteString(type_) - - // nested types - for _, t := range t.types { - if t.name != structName { - result.WriteString(t.def) - result.WriteString("\n") - } - } - - return result.String(), nil + type_ := t.getTypeAnnotation(structName, input) + return type_, nil } -func (t *Transformer) generateTypeAnnotation(typeName string, input any) string { +func (t *Transformer) getTypeAnnotation(typeName string, input any) string { switch v := input.(type) { case map[string]any: - return t.buildStruct(typeName, v) + return fmt.Sprintf("type %s %s", typeName, t.buildStruct(v)) case []any: if len(v) == 0 {@@ -75,9 +58,6 @@ return fmt.Sprintf("type %s float64", typeName)
case bool: return fmt.Sprintf("type %s bool", typeName) - - case nil: - return fmt.Sprintf("type %s any", typeName) default: return fmt.Sprintf("type %s any", typeName)@@ -86,18 +66,18 @@ }
} // todo: input shouldn't be map, to preserve it's order -func (t *Transformer) buildStruct(typeName string, input map[string]any) string { +func (t *Transformer) buildStruct(input map[string]any) string { var fields strings.Builder - for key, value := range input { - fieldName := t.toGoFieldName(key) + for _, f := range mapToStructInput(input) { + fieldName := t.toGoFieldName(f.field) if fieldName == "" { fieldName = "Field" } - fieldType := t.getGoType(fieldName, value) + fieldType := t.getGoType(fieldName, f.type_) // todo: toggle json tags generation - jsonTag := fmt.Sprintf("`json:\"%s\"`", key) + jsonTag := fmt.Sprintf("`json:\"%s\"`", f.field) // todo: figure out the indentation, since it might have nested struct fields.WriteString(fmt.Sprintf(@@ -108,30 +88,20 @@ jsonTag,
)) } - structDef := fmt.Sprintf("type %s struct {\n%s}", typeName, fields.String()) - t.types = append(t.types, types{ - name: typeName, - def: structDef, - }) - - return structDef + return fmt.Sprintf("struct {\n%s}", fields.String()) } func (t *Transformer) getGoType(fieldName string, value any) string { switch v := value.(type) { case map[string]any: - typeName := t.toGoTypeName(fieldName) - if !t.isTypeRecorded(typeName) { - t.generateTypeAnnotation(typeName, v) - } - return typeName + return t.buildStruct(v) case []any: if len(v) == 0 { return "[]any" } - type_ := t.getGoType(fieldName+"Item", v[0]) // TODO + type_ := t.getGoType(fieldName, v[0]) return "[]" + type_ case float64:@@ -145,23 +115,12 @@ return "string"
case bool: return "bool" - - case nil: - return "any" default: return "any" } } -func (t *Transformer) toGoTypeName(fieldName string) string { - goName := t.toGoFieldName(fieldName) - if len(goName) > 0 { - return strings.ToUpper(goName[:1]) + goName[1:] - } - return "Type" -} - func (t *Transformer) toGoFieldName(jsonField string) string { parts := strings.Split(jsonField, "_")@@ -177,11 +136,20 @@
return result.String() } -func (t *Transformer) isTypeRecorded(name string) bool { - for _, t := range t.types { - if t.name == name { - return true - } +type structInput struct { + field string + type_ any +} + +func mapToStructInput(input map[string]any) []structInput { + res := make([]structInput, 0, len(input)) + for k, v := range input { + res = append(res, structInput{k, v}) } - return false + + sort.Slice(res, func(i, j int) bool { + return res[i].field < res[j].field + }) + + return res }
M
json2go_test.go
@@ -3,11 +3,16 @@
import ( "errors" "fmt" + "reflect" "strings" "testing" ) func field(name, type_ string, json_ ...string) string { + if strings.Contains(type_, "struct") { + return fmt.Sprintf("\n%s %s", name, type_) + } + tag := strings.ToLower(name) if len(json_) == 1 { tag = json_[0]@@ -24,10 +29,10 @@ }{
"simple object": { input: `{"name": "Olex", "active": true, "age": 420}`, output: "type Out struct {" + - field("Name", "string") + field("Active", "bool") + field("Age", "int") + - "\n}\n", + field("Name", "string") + + "\n}", }, "invalid json": { err: ErrInvalidJSON,@@ -38,48 +43,50 @@ input: `{"first_name": "Bob", "last_name": "Bobberson"}`,
output: "type Out struct {" + field("FirstName", "string", "first_name") + field("LastName", "string", "last_name") + - "\n}\n", + "\n}", }, "nested object and array": { input: `{"user": {"name": "Alice", "score": 95.5}, "tags": ["go", "json"]}`, output: "type Out struct {" + - field("User", "User") + field("Tags", "[]string") + - "\n}\ntype User struct {" + + field("User", "struct {") + field("Name", "string") + field("Score", "float64") + - "\n}\n", + "\n} `json:\"user\"`" + + "\n}", }, "empty nested object": { input: `{"user": {}}`, output: "type Out struct {" + - field("User", "User") + - "\n}\ntype User struct {\n}\n", + field("User", "struct {") + + "\n} `json:\"user\"`" + + "\n}", }, "array of object": { input: `[{"name": "John"}, {"name": "Jane"}]`, - output: "type Out []OutItem" + - "\ntype OutItem struct {" + + output: "type Out []struct {" + field("Name", "string") + - "\n}\n", + "\n}", }, "empty array": { input: `{"items": []}`, - output: `type Out struct {` + - field("Items", "[]any") + "\n}\n", + output: "type Out struct {" + + field("Items", "[]any") + + "\n}", }, "null": { input: `{"item": null}`, output: `type Out struct {` + - field("Item", "any") + "\n}\n", + field("Item", "any") + + "\n}", }, "numbers": { input: `{"pos": 123, "neg": -321, "float": 420.69}`, output: "type Out struct {" + - field("Pos", "int") + - field("Neg", "int") + field("Float", "float64") + - "\n}\n", + field("Neg", "int") + + field("Pos", "int") + + "\n}", }, }@@ -88,20 +95,7 @@ for tname, tt := range tests {
t.Run(tname, func(t *testing.T) { result, err := trans.Transform("Out", tt.input) assertEqualErr(t, tt.err, err) - - lines := strings.Split(result, "\n") - counts := make(map[string]int) - for _, line := range lines { - if !strings.Contains(line, "}") { - counts[line]++ - } - } - - for _, line := range lines { - if counts[line] > 1 { - t.Fatalf("found duplicate line: %s", line) - } - } + assertEqual(t, tt.output, result) }) } }@@ -112,3 +106,10 @@ if (expected != nil || actual != nil) && errors.Is(expected, actual) {
t.Errorf("expected: %v, got: %v\n", expected, actual) } } + +func assertEqual[T any](t *testing.T, expected, actual T) { + t.Helper() + if !reflect.DeepEqual(expected, actual) { + t.Errorf("expected: %v, got: %v\n", expected, actual) + } +}