+26
.github/workflows/go.yml
+26
.github/workflows/go.yml
···
1
+
name: go
2
+
on:
3
+
workflow_dispatch:
4
+
push:
5
+
branches: [main]
6
+
paths: ["**.go", "go.mod", "go.sum"]
7
+
pull_request:
8
+
paths: ["**.go", "go.mod", "go.sum"]
9
+
10
+
jobs:
11
+
release:
12
+
runs-on: ubuntu-latest
13
+
steps:
14
+
- uses: actions/checkout@v5
15
+
16
+
- name: setup go
17
+
uses: actions/setup-go@v5
18
+
with:
19
+
go-version-file: go.mod
20
+
cache-dependency-path: go.mod
21
+
22
+
- name: build
23
+
run: go build ./cmd/json2go
24
+
25
+
- name: test
26
+
run: go test -v ./...
+9
cmd/json2go/main.go
+9
cmd/json2go/main.go
+187
json2go.go
+187
json2go.go
···
1
+
package json2go
2
+
3
+
import (
4
+
"encoding/json"
5
+
"errors"
6
+
"fmt"
7
+
"strings"
8
+
)
9
+
10
+
var ErrInvalidJSON = errors.New("invalid json")
11
+
12
+
type (
13
+
types struct{ name, def string }
14
+
Transformer struct {
15
+
structName string
16
+
types []types
17
+
}
18
+
)
19
+
20
+
func NewTransformer() *Transformer {
21
+
return &Transformer{}
22
+
}
23
+
24
+
// Transform ...
25
+
// todo: take io.Reader as input?
26
+
// todo: output as io.Writer?
27
+
// todo: validate provided structName
28
+
func (t *Transformer) Transform(structName, jsonStr string) (string, error) {
29
+
t.structName = structName
30
+
t.types = make([]types, 1)
31
+
32
+
var input any
33
+
if err := json.Unmarshal([]byte(jsonStr), &input); err != nil {
34
+
return "", errors.Join(ErrInvalidJSON, err)
35
+
}
36
+
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
52
+
}
53
+
54
+
func (t *Transformer) generateTypeAnnotation(typeName string, input any) string {
55
+
switch v := input.(type) {
56
+
case map[string]any:
57
+
return t.buildStruct(typeName, v)
58
+
59
+
case []any:
60
+
if len(v) == 0 {
61
+
return fmt.Sprintf("type %s []any", t.structName)
62
+
}
63
+
64
+
type_ := t.getGoType(typeName+"Item", v[0])
65
+
return fmt.Sprintf("type %s []%s", typeName, type_)
66
+
67
+
case string:
68
+
return fmt.Sprintf("type %s string", typeName)
69
+
70
+
case float64:
71
+
if float64(int(v)) == v {
72
+
return fmt.Sprintf("type %s int", typeName)
73
+
}
74
+
return fmt.Sprintf("type %s float64", typeName)
75
+
76
+
case bool:
77
+
return fmt.Sprintf("type %s bool", typeName)
78
+
79
+
case nil:
80
+
return fmt.Sprintf("type %s any", typeName)
81
+
82
+
default:
83
+
return fmt.Sprintf("type %s any", typeName)
84
+
85
+
}
86
+
}
87
+
88
+
// todo: input shouldn't be map, to preserve it's order
89
+
func (t *Transformer) buildStruct(typeName string, input map[string]any) string {
90
+
var fields strings.Builder
91
+
for key, value := range input {
92
+
fieldName := t.toGoFieldName(key)
93
+
if fieldName == "" {
94
+
fieldName = "Field"
95
+
}
96
+
97
+
fieldType := t.getGoType(fieldName, value)
98
+
99
+
// todo: toggle json tags generation
100
+
jsonTag := fmt.Sprintf("`json:\"%s\"`", key)
101
+
102
+
// todo: figure out the indentation, since it might have nested struct
103
+
fields.WriteString(fmt.Sprintf(
104
+
"%s %s %s\n",
105
+
fieldName,
106
+
fieldType,
107
+
jsonTag,
108
+
))
109
+
}
110
+
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
118
+
}
119
+
120
+
func (t *Transformer) getGoType(fieldName string, value any) string {
121
+
switch v := value.(type) {
122
+
case map[string]any:
123
+
typeName := t.toGoTypeName(fieldName)
124
+
if !t.isTypeRecorded(typeName) {
125
+
t.generateTypeAnnotation(typeName, v)
126
+
}
127
+
return typeName
128
+
129
+
case []any:
130
+
if len(v) == 0 {
131
+
return "[]any"
132
+
}
133
+
134
+
type_ := t.getGoType(fieldName+"Item", v[0]) // TODO
135
+
return "[]" + type_
136
+
137
+
case float64:
138
+
if float64(int(v)) == v {
139
+
return "int"
140
+
}
141
+
return "float64"
142
+
143
+
case string:
144
+
return "string"
145
+
146
+
case bool:
147
+
return "bool"
148
+
149
+
case nil:
150
+
return "any"
151
+
152
+
default:
153
+
return "any"
154
+
}
155
+
}
156
+
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
+
func (t *Transformer) toGoFieldName(jsonField string) string {
166
+
parts := strings.Split(jsonField, "_")
167
+
168
+
var result strings.Builder
169
+
for _, part := range parts {
170
+
if part != "" {
171
+
if len(part) > 0 {
172
+
result.WriteString(strings.ToUpper(part[:1]) + part[1:])
173
+
}
174
+
}
175
+
}
176
+
177
+
return result.String()
178
+
}
179
+
180
+
func (t *Transformer) isTypeRecorded(name string) bool {
181
+
for _, t := range t.types {
182
+
if t.name == name {
183
+
return true
184
+
}
185
+
}
186
+
return false
187
+
}
+114
json2go_test.go
+114
json2go_test.go
···
1
+
package json2go
2
+
3
+
import (
4
+
"errors"
5
+
"fmt"
6
+
"strings"
7
+
"testing"
8
+
)
9
+
10
+
func field(name, type_ string, json_ ...string) string {
11
+
tag := strings.ToLower(name)
12
+
if len(json_) == 1 {
13
+
tag = json_[0]
14
+
}
15
+
return fmt.Sprintf("\n%s %s `json:\"%s\"`", name, type_, tag)
16
+
}
17
+
18
+
func TestTransformer_Transform(t *testing.T) {
19
+
tests := map[string]struct {
20
+
input string
21
+
output string
22
+
err error
23
+
}{
24
+
"simple object": {
25
+
input: `{"name": "Olex", "active": true, "age": 420}`,
26
+
output: "type Out struct {" +
27
+
field("Name", "string") +
28
+
field("Active", "bool") +
29
+
field("Age", "int") +
30
+
"\n}\n",
31
+
},
32
+
"invalid json": {
33
+
err: ErrInvalidJSON,
34
+
input: `{"invalid":json}`,
35
+
},
36
+
"snake_case to CamelCase": {
37
+
input: `{"first_name": "Bob", "last_name": "Bobberson"}`,
38
+
output: "type Out struct {" +
39
+
field("FirstName", "string", "first_name") +
40
+
field("LastName", "string", "last_name") +
41
+
"\n}\n",
42
+
},
43
+
"nested object and array": {
44
+
input: `{"user": {"name": "Alice", "score": 95.5}, "tags": ["go", "json"]}`,
45
+
output: "type Out struct {" +
46
+
field("User", "User") +
47
+
field("Tags", "[]string") +
48
+
"\n}\ntype User struct {" +
49
+
field("Name", "string") +
50
+
field("Score", "float64") +
51
+
"\n}\n",
52
+
},
53
+
"empty nested object": {
54
+
input: `{"user": {}}`,
55
+
output: "type Out struct {" +
56
+
field("User", "User") +
57
+
"\n}\ntype User struct {\n}\n",
58
+
},
59
+
"array of object": {
60
+
input: `[{"name": "John"}, {"name": "Jane"}]`,
61
+
output: "type Out []OutItem" +
62
+
"\ntype OutItem struct {" +
63
+
field("Name", "string") +
64
+
"\n}\n",
65
+
},
66
+
"empty array": {
67
+
input: `{"items": []}`,
68
+
output: `type Out struct {` +
69
+
field("Items", "[]any") + "\n}\n",
70
+
},
71
+
"null": {
72
+
input: `{"item": null}`,
73
+
output: `type Out struct {` +
74
+
field("Item", "any") + "\n}\n",
75
+
},
76
+
"numbers": {
77
+
input: `{"pos": 123, "neg": -321, "float": 420.69}`,
78
+
output: "type Out struct {" +
79
+
field("Pos", "int") +
80
+
field("Neg", "int") +
81
+
field("Float", "float64") +
82
+
"\n}\n",
83
+
},
84
+
}
85
+
86
+
trans := NewTransformer()
87
+
for tname, tt := range tests {
88
+
t.Run(tname, func(t *testing.T) {
89
+
result, err := trans.Transform("Out", tt.input)
90
+
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
+
}
105
+
})
106
+
}
107
+
}
108
+
109
+
func assertEqualErr(t *testing.T, expected, actual error) {
110
+
t.Helper()
111
+
if (expected != nil || actual != nil) && errors.Is(expected, actual) {
112
+
t.Errorf("expected: %v, got: %v\n", expected, actual)
113
+
}
114
+
}
+16
readme.txt
+16
readme.txt
···
1
+
json2go
2
+
-------
3
+
4
+
json2go to go provides a library and cli tool for
5
+
convening json strings to go struct definitions
6
+
7
+
t := json2go. NewTransformer()
8
+
typedef, _ := t.Transform(jsonStr, "TypeName")
9
+
10
+
11
+
cli interface:
12
+
13
+
go install olexsmir.xyz/json2go/cmd/json2go@latest
14
+
15
+
echo "{...}" | json2go
16
+
json2go "{...}"