package json2go import ( "encoding/json" "errors" "fmt" "strings" ) var ErrInvalidJSON = errors.New("invalid json") type ( types struct{ name, def string } Transformer struct { structName string types []types } ) func NewTransformer() *Transformer { return &Transformer{} } // Transform ... // todo: take io.Reader as input? // todo: output as io.Writer? // todo: validate provided structName func (t *Transformer) Transform(structName, jsonStr string) (string, error) { t.structName = structName t.types = make([]types, 1) var input any if err := json.Unmarshal([]byte(jsonStr), &input); err != nil { return "", errors.Join(ErrInvalidJSON, err) } var result strings.Builder // the "parent" type type_ := t.generateTypeAnnotation(structName, input) result.WriteString(type_) // nested types for _, t := range t.types { if t.name != structName { result.WriteString(t.def) result.WriteString("\n") } } return result.String(), nil } func (t *Transformer) generateTypeAnnotation(typeName string, input any) string { switch v := input.(type) { case map[string]any: return t.buildStruct(typeName, v) case []any: if len(v) == 0 { return fmt.Sprintf("type %s []any", t.structName) } type_ := t.getGoType(typeName+"Item", v[0]) return fmt.Sprintf("type %s []%s", typeName, type_) case string: return fmt.Sprintf("type %s string", typeName) case float64: if float64(int(v)) == v { return fmt.Sprintf("type %s int", typeName) } return fmt.Sprintf("type %s float64", typeName) case bool: return fmt.Sprintf("type %s bool", typeName) case nil: return fmt.Sprintf("type %s any", typeName) default: return fmt.Sprintf("type %s any", typeName) } } // todo: input shouldn't be map, to preserve it's order func (t *Transformer) buildStruct(typeName string, input map[string]any) string { var fields strings.Builder for key, value := range input { fieldName := t.toGoFieldName(key) if fieldName == "" { fieldName = "Field" } fieldType := t.getGoType(fieldName, value) // todo: toggle json tags generation jsonTag := fmt.Sprintf("`json:\"%s\"`", key) // todo: figure out the indentation, since it might have nested struct fields.WriteString(fmt.Sprintf( "%s %s %s\n", fieldName, fieldType, jsonTag, )) } structDef := fmt.Sprintf("type %s struct {\n%s}", typeName, fields.String()) t.types = append(t.types, types{ name: typeName, def: structDef, }) return structDef } func (t *Transformer) getGoType(fieldName string, value any) string { switch v := value.(type) { case map[string]any: typeName := t.toGoTypeName(fieldName) if !t.isTypeRecorded(typeName) { t.generateTypeAnnotation(typeName, v) } return typeName case []any: if len(v) == 0 { return "[]any" } type_ := t.getGoType(fieldName+"Item", v[0]) // TODO return "[]" + type_ case float64: if float64(int(v)) == v { return "int" } return "float64" case string: return "string" case bool: return "bool" case nil: return "any" default: return "any" } } func (t *Transformer) toGoTypeName(fieldName string) string { goName := t.toGoFieldName(fieldName) if len(goName) > 0 { return strings.ToUpper(goName[:1]) + goName[1:] } return "Type" } func (t *Transformer) toGoFieldName(jsonField string) string { parts := strings.Split(jsonField, "_") var result strings.Builder for _, part := range parts { if part != "" { if len(part) > 0 { result.WriteString(strings.ToUpper(part[:1]) + part[1:]) } } } return result.String() } func (t *Transformer) isTypeRecorded(name string) bool { for _, t := range t.types { if t.name == name { return true } } return false }