all repos

json2go @ 34a739e

convert json to go type annotations

json2go/json2go.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
some polishing, 6 months ago
1
package json2go
2
3
import (
4
	"encoding/json"
5
	"errors"
6
	"fmt"
7
	"regexp"
8
	"sort"
9
	"strings"
10
)
11
12
var identRe = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
13
var (
14
	ErrInvalidJSON       = errors.New("invalid json")
15
	ErrInvalidStructName = errors.New("invalid struct name")
16
)
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) {
30
		return "", ErrInvalidStructName
31
	}
32
33
	t.structName = structName
34
	t.currentIndent = 0
35
36
	var input any
37
	if err := json.Unmarshal([]byte(jsonStr), &input); err != nil {
38
		return "", errors.Join(ErrInvalidJSON, err)
39
	}
40
41
	type_ := t.getTypeAnnotation(structName, input)
42
	return type_, nil
43
}
44
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
		))
100
	}
101
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"
134
	}
135
}
136
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
			}
146
		}
147
	}
148
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
168
}