package json2go import ( "encoding/json" "errors" "fmt" "regexp" "sort" "strings" ) var identRe = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) var ( ErrInvalidJSON = errors.New("invalid json") ErrInvalidStructName = errors.New("invalid struct name") ) type Transformer struct { structName string currentIndent int } func NewTransformer() *Transformer { return &Transformer{} } // Transform transforms provided json string into go type annotation func (t *Transformer) Transform(structName, jsonStr string) (string, error) { if !identRe.MatchString(structName) { return "", ErrInvalidStructName } t.structName = structName t.currentIndent = 0 var input any if err := json.Unmarshal([]byte(jsonStr), &input); err != nil { return "", errors.Join(ErrInvalidJSON, err) } type_ := t.getTypeAnnotation(structName, input) return type_, nil } func (t *Transformer) getTypeAnnotation(typeName string, input any) string { switch v := input.(type) { case map[string]any: return fmt.Sprintf("type %s %s", typeName, t.buildStruct(v)) case []any: if len(v) == 0 { return fmt.Sprintf("type %s []any", typeName) } 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) default: return fmt.Sprintf("type %s any", typeName) } } func (t *Transformer) buildStruct(input map[string]any) string { var fields strings.Builder for _, f := range mapToStructInput(input) { fieldName := t.toGoFieldName(f.field) if fieldName == "" { fieldName = "NotNamedField" f.field = "NotNamedField" } // increase indentation in case of building new struct t.currentIndent++ fieldType := t.getGoType(fieldName, f.type_) t.currentIndent-- jsonTag := fmt.Sprintf("`json:\"%s\"`", f.field) indent := strings.Repeat("\t", t.currentIndent+1) fields.WriteString(fmt.Sprintf( "%s%s %s %s\n", indent, fieldName, fieldType, jsonTag, )) } return fmt.Sprintf("struct {\n%s%s}", fields.String(), strings.Repeat("\t", t.currentIndent)) } func (t *Transformer) getGoType(fieldName string, value any) string { switch v := value.(type) { case map[string]any: return t.buildStruct(v) case []any: if len(v) == 0 { return "[]any" } type_ := t.getGoType(fieldName, v[0]) return "[]" + type_ case float64: if float64(int(v)) == v { return "int" } return "float64" case string: return "string" case bool: return "bool" default: return "any" } } 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() } type structInput struct { field string type_ any } func mapToStructInput(input map[string]any) []structInput { res := make([]structInput, 0, len(input)) for k, v := range input { res = append(res, structInput{k, v}) } sort.Slice(res, func(i, j int) bool { return res[i].field < res[j].field }) return res }