[mirror] convert json to go types olexsmir.xyz/json2go

init

Changed files
+355
.github
workflows
cmd
json2go
+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
··· 1 + package main 2 + 3 + // todo: piped / sedin 4 + // todo: passed as argument 5 + // todo: read from file 6 + // todo: set name of the type 7 + 8 + func main() { 9 + }
+3
go.mod
··· 1 + module olexsmir.xyz/json2go 2 + 3 + go 1.25.3
+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
··· 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
··· 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 "{...}"