all repos

json2go @ af83030

convert json to go type annotations
6 files changed, 355 insertions(+), 0 deletions(-)
init
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2025-11-26 19:49:04 +0200
Authored at: 2025-11-24 19:58:40 +0200
Change ID: qmytrmvzkqwpnurmwxxmtpuyqqxqqpur
A .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 ./...

      
A 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
        +}

      
A go.mod
···
        
        1
        +module olexsmir.xyz/json2go

      
        
        2
        +

      
        
        3
        +go 1.25.3

      
A 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
        +}

      
A 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
        +}

      
A 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 "{...}"