all repos

json2go @ 7a527f3f3ebf35fb7c5fadf0d3d93a1c79557d6e

convert json to go type annotations
2 files changed, 63 insertions(+), 94 deletions(-)
refactor: inline all types in one struct
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
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
        +}