Skip to content

Commit befd5cc

Browse files
authored
fix: Make the JSON type schema match the JSON that the encoder writes (#2620)
#### Summary The JSON type schema now moves embedded struct fields up into the parent, skips `json:"-"` fields, and writes `json` or `utf8` for a type with its own marshaller unless its `json` tags describe the output, as the encoder does. Fixes https://linear.app/env-zero/issue/ATH-913
1 parent fc17c1b commit befd5cc

3 files changed

Lines changed: 914 additions & 17 deletions

File tree

‎transformers/json_schema_fields.go‎

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package transformers
2+
3+
import (
4+
"reflect"
5+
"strings"
6+
)
7+
8+
type jsonSchemaField struct {
9+
field reflect.StructField
10+
depth int
11+
tagged bool
12+
addressable bool
13+
}
14+
15+
type embeddedJSONStruct struct {
16+
structType reflect.Type
17+
count int
18+
addressable bool
19+
}
20+
21+
type jsonSchemaFieldsKey struct {
22+
structType reflect.Type
23+
addressable bool
24+
}
25+
26+
func (t *structTransformer) jsonSchemaFields(structType reflect.Type, addressable bool) map[string]jsonSchemaField {
27+
key := jsonSchemaFieldsKey{structType: structType, addressable: addressable}
28+
if fields, ok := t.jsonSchemaFieldsCache[key]; ok {
29+
return fields
30+
}
31+
if t.jsonSchemaFieldsCache == nil {
32+
t.jsonSchemaFieldsCache = make(map[jsonSchemaFieldsKey]map[string]jsonSchemaField)
33+
}
34+
fields := t.computeJSONSchemaFields(structType, addressable)
35+
t.jsonSchemaFieldsCache[key] = fields
36+
return fields
37+
}
38+
39+
func (t *structTransformer) computeJSONSchemaFields(structType reflect.Type, addressable bool) map[string]jsonSchemaField {
40+
candidates := make(map[string][]jsonSchemaField)
41+
visited := make(map[reflect.Type]bool)
42+
level := []*embeddedJSONStruct{{structType: structType, count: 1, addressable: addressable}}
43+
for depth := 0; len(level) > 0; depth++ {
44+
for _, embedded := range level {
45+
visited[embedded.structType] = true
46+
}
47+
var next []*embeddedJSONStruct
48+
nextByType := make(map[reflect.Type]*embeddedJSONStruct)
49+
for _, embedded := range level {
50+
for i := 0; i < embedded.structType.NumField(); i++ {
51+
structField := embedded.structType.Field(i)
52+
if isTypeIgnored(structField.Type) {
53+
continue
54+
}
55+
fieldType := structField.Type
56+
if fieldType.Name() == "" && fieldType.Kind() == reflect.Pointer {
57+
fieldType = fieldType.Elem()
58+
}
59+
tagName, _, _ := strings.Cut(structField.Tag.Get("json"), ",")
60+
isEmbeddedStruct := structField.Anonymous && fieldType.Kind() == reflect.Struct
61+
if isEmbeddedStruct && tagName == "" {
62+
if visited[fieldType] {
63+
continue
64+
}
65+
reached, ok := nextByType[fieldType]
66+
if !ok {
67+
reached = &embeddedJSONStruct{
68+
structType: fieldType,
69+
addressable: embedded.addressable || structField.Type.Kind() == reflect.Pointer,
70+
}
71+
nextByType[fieldType] = reached
72+
next = append(next, reached)
73+
}
74+
reached.count++
75+
continue
76+
}
77+
if !structField.IsExported() && !isEmbeddedStruct {
78+
continue
79+
}
80+
name, err := t.jsonSchemaNameTransformer(structField)
81+
if err != nil || name == "" {
82+
continue
83+
}
84+
candidate := jsonSchemaField{
85+
field: structField,
86+
depth: depth,
87+
tagged: tagName != "",
88+
addressable: embedded.addressable,
89+
}
90+
candidates[name] = append(candidates[name], candidate)
91+
if embedded.count > 1 {
92+
candidates[name] = append(candidates[name], candidate)
93+
}
94+
}
95+
}
96+
level = next
97+
}
98+
99+
fields := make(map[string]jsonSchemaField, len(candidates))
100+
for name, sameName := range candidates {
101+
if winner, ok := dominantJSONSchemaField(sameName); ok {
102+
fields[name] = winner
103+
}
104+
}
105+
return fields
106+
}
107+
108+
func dominantJSONSchemaField(sameName []jsonSchemaField) (jsonSchemaField, bool) {
109+
shallowest := sameName[0].depth
110+
for _, f := range sameName {
111+
shallowest = min(shallowest, f.depth)
112+
}
113+
var atShallowest, tagged []jsonSchemaField
114+
for _, f := range sameName {
115+
if f.depth != shallowest {
116+
continue
117+
}
118+
atShallowest = append(atShallowest, f)
119+
if f.tagged {
120+
tagged = append(tagged, f)
121+
}
122+
}
123+
switch {
124+
case len(atShallowest) == 1:
125+
return atShallowest[0], true
126+
case len(tagged) == 1:
127+
return tagged[0], true
128+
default:
129+
return jsonSchemaField{}, false
130+
}
131+
}

0 commit comments

Comments
 (0)