Skip to content

Commit db3fa0c

Browse files
authored
Merge pull request #1589 from lanej/feature/array-element-discrimination
feat(gen): add array element type discrimination for oneOf/anyOf
2 parents 894cc34 + 8b4a188 commit db3fa0c

File tree

6 files changed

+468
-12
lines changed

6 files changed

+468
-12
lines changed
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
{
2+
"openapi": "3.0.3",
3+
"info": {
4+
"title": "Array Element Type Discrimination Test",
5+
"version": "1.0.0",
6+
"description": "Tests discrimination between oneOf variants based on array element types (currently unsupported but should work)"
7+
},
8+
"paths": {
9+
"/basic-arrays": {
10+
"get": {
11+
"operationId": "getBasicArrays",
12+
"description": "Test basic array element discrimination: string[] vs integer[]",
13+
"responses": {
14+
"200": {
15+
"description": "OK",
16+
"content": {
17+
"application/json": {
18+
"schema": {
19+
"type": "array",
20+
"items": {
21+
"$ref": "#/components/schemas/BasicArrayResource"
22+
}
23+
}
24+
}
25+
}
26+
}
27+
}
28+
}
29+
},
30+
"/object-vs-primitive-arrays": {
31+
"get": {
32+
"operationId": "getObjectVsPrimitiveArrays",
33+
"description": "Test array of objects vs array of primitives",
34+
"responses": {
35+
"200": {
36+
"description": "OK",
37+
"content": {
38+
"application/json": {
39+
"schema": {
40+
"type": "array",
41+
"items": {
42+
"$ref": "#/components/schemas/ObjectVsPrimitiveResource"
43+
}
44+
}
45+
}
46+
}
47+
}
48+
}
49+
}
50+
},
51+
"/mixed-discrimination": {
52+
"post": {
53+
"operationId": "postMixedDiscrimination",
54+
"description": "Test mixed discrimination with array field + other fields",
55+
"requestBody": {
56+
"required": true,
57+
"content": {
58+
"application/json": {
59+
"schema": {
60+
"$ref": "#/components/schemas/MixedDiscriminationResource"
61+
}
62+
}
63+
}
64+
},
65+
"responses": {
66+
"200": {
67+
"description": "OK"
68+
}
69+
}
70+
}
71+
}
72+
},
73+
"components": {
74+
"schemas": {
75+
"BasicArrayResource": {
76+
"oneOf": [
77+
{
78+
"$ref": "#/components/schemas/StringArrayVariant"
79+
},
80+
{
81+
"$ref": "#/components/schemas/IntegerArrayVariant"
82+
},
83+
{
84+
"$ref": "#/components/schemas/BooleanArrayVariant"
85+
}
86+
]
87+
},
88+
"StringArrayVariant": {
89+
"type": "object",
90+
"required": ["name", "items"],
91+
"properties": {
92+
"name": {
93+
"type": "string"
94+
},
95+
"items": {
96+
"type": "array",
97+
"description": "Array of string items",
98+
"items": {
99+
"type": "string"
100+
}
101+
},
102+
"stringInfo": {
103+
"type": "string",
104+
"description": "Unique to string variant for fallback discrimination"
105+
}
106+
}
107+
},
108+
"IntegerArrayVariant": {
109+
"type": "object",
110+
"required": ["name", "items"],
111+
"properties": {
112+
"name": {
113+
"type": "string"
114+
},
115+
"items": {
116+
"type": "array",
117+
"description": "Array of integer items",
118+
"items": {
119+
"type": "integer"
120+
}
121+
},
122+
"intInfo": {
123+
"type": "integer",
124+
"description": "Unique to integer variant for fallback discrimination"
125+
}
126+
}
127+
},
128+
"BooleanArrayVariant": {
129+
"type": "object",
130+
"required": ["name", "items"],
131+
"properties": {
132+
"name": {
133+
"type": "string"
134+
},
135+
"items": {
136+
"type": "array",
137+
"description": "Array of boolean items",
138+
"items": {
139+
"type": "boolean"
140+
}
141+
},
142+
"boolInfo": {
143+
"type": "boolean",
144+
"description": "Unique to boolean variant for fallback discrimination"
145+
}
146+
}
147+
},
148+
"ObjectVsPrimitiveResource": {
149+
"oneOf": [
150+
{
151+
"$ref": "#/components/schemas/ObjectArrayVariant"
152+
},
153+
{
154+
"$ref": "#/components/schemas/PrimitiveArrayVariant"
155+
}
156+
]
157+
},
158+
"ObjectArrayVariant": {
159+
"type": "object",
160+
"required": ["id", "items"],
161+
"properties": {
162+
"id": {
163+
"type": "string"
164+
},
165+
"items": {
166+
"type": "array",
167+
"description": "Array of object items",
168+
"items": {
169+
"type": "object",
170+
"required": ["key", "value"],
171+
"properties": {
172+
"key": {
173+
"type": "string"
174+
},
175+
"value": {
176+
"type": "integer"
177+
}
178+
}
179+
}
180+
},
181+
"objectMeta": {
182+
"type": "object",
183+
"description": "Unique to object variant for fallback discrimination",
184+
"properties": {
185+
"version": {
186+
"type": "integer"
187+
}
188+
}
189+
}
190+
}
191+
},
192+
"PrimitiveArrayVariant": {
193+
"type": "object",
194+
"required": ["id", "items"],
195+
"properties": {
196+
"id": {
197+
"type": "string"
198+
},
199+
"items": {
200+
"type": "array",
201+
"description": "Array of primitive string items",
202+
"items": {
203+
"type": "string"
204+
}
205+
},
206+
"primitiveMeta": {
207+
"type": "string",
208+
"description": "Unique to primitive variant for fallback discrimination"
209+
}
210+
}
211+
},
212+
"MixedDiscriminationResource": {
213+
"oneOf": [
214+
{
215+
"$ref": "#/components/schemas/MixedStringArrayVariant"
216+
},
217+
{
218+
"$ref": "#/components/schemas/MixedNumberArrayVariant"
219+
}
220+
]
221+
},
222+
"MixedStringArrayVariant": {
223+
"type": "object",
224+
"required": ["id", "items", "metadata"],
225+
"properties": {
226+
"id": {
227+
"type": "string"
228+
},
229+
"items": {
230+
"type": "array",
231+
"description": "Array of strings",
232+
"items": {
233+
"type": "string"
234+
}
235+
},
236+
"metadata": {
237+
"type": "string",
238+
"description": "Unique to string variant"
239+
}
240+
}
241+
},
242+
"MixedNumberArrayVariant": {
243+
"type": "object",
244+
"required": ["id", "items", "count"],
245+
"properties": {
246+
"id": {
247+
"type": "string"
248+
},
249+
"items": {
250+
"type": "array",
251+
"description": "Array of numbers",
252+
"items": {
253+
"type": "number"
254+
}
255+
},
256+
"count": {
257+
"type": "integer",
258+
"description": "Unique to number variant"
259+
}
260+
}
261+
}
262+
}
263+
}
264+
}

gen/_template/json/encoders_sum.tmpl

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,46 @@ func (s *{{ $.Name }}) Decode(d *jx.Decoder) error {
153153
}
154154
found = true
155155
s.Type = match
156+
{{- else if needsArrayElementDiscrimination $variants }}
157+
// Array element discrimination: peek into array to check first element type
158+
if typ := d.Next(); typ != jx.Array {
159+
return d.Skip()
160+
}
161+
// Capture array to peek at first element without consuming
162+
if err := d.Capture(func(d *jx.Decoder) error {
163+
// Check if array is empty
164+
iter, err := d.ArrIter()
165+
if err != nil {
166+
return err
167+
}
168+
if !iter.Next() {
169+
// Empty array - use first variant as default
170+
{{- $firstVariant := index (dedupeVariantsByArrayElementType $variants) 0 }}
171+
if !found {
172+
found = true
173+
s.Type = {{ $firstVariant.VariantType }}
174+
}
175+
return nil
176+
}
177+
elemType := d.Next()
178+
switch elemType {
179+
{{- range $v := dedupeVariantsByArrayElementType $variants }}
180+
{{- if $v.ArrayElementType }}
181+
case {{ $v.ArrayElementType }}:
182+
match := {{ $v.VariantType }}
183+
if found && s.Type != match {
184+
s.Type = ""
185+
return errors.Errorf("multiple oneOf matches: (%v, %v)", s.Type, match)
186+
}
187+
found = true
188+
s.Type = match
189+
{{- end }}
190+
{{- end }}
191+
}
192+
return nil
193+
}); err != nil {
194+
return err
195+
}
156196
{{- else }}
157197
// Multiple variants have this field - use type checking to discriminate
158198
typ := d.Next()

gen/ir/type.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,15 @@ type UniqueFieldVariant struct {
3838
VariantType string // e.g., "SystemEventEvent"
3939
FieldType string // jx.Type constant, e.g., "jx.String"
4040
Nullable bool // true if field is nullable (accepts both base type and jx.Null)
41+
42+
// ArrayElementType is the jx.Type of array elements for array element discrimination.
43+
// Only set when FieldType is "jx.Array" and element type can distinguish variants.
44+
// e.g., "jx.String" for array[string], "jx.Number" for array[integer], "jx.Object" for array[object]
45+
ArrayElementType string
46+
47+
// ArrayElementTypeID is the full type ID for array elements (e.g., "string", "integer", "object").
48+
// Used for more detailed discrimination like distinguishing integer vs number.
49+
ArrayElementTypeID string
4150
}
4251

4352
// SumSpec for KindSum.

gen/schema_gen.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"cmp"
55
"fmt"
66
"path"
7+
"sort"
78
"strings"
89

910
"github.com/go-faster/errors"
@@ -865,7 +866,14 @@ func inferSchemaFromObject(obj map[string]any) *jsonschema.Schema {
865866
schema := &jsonschema.Schema{
866867
Type: jsonschema.Object,
867868
}
868-
for fieldName, fieldValue := range obj {
869+
// Sort keys for deterministic output
870+
keys := make([]string, 0, len(obj))
871+
for k := range obj {
872+
keys = append(keys, k)
873+
}
874+
sort.Strings(keys)
875+
for _, fieldName := range keys {
876+
fieldValue := obj[fieldName]
869877
prop := jsonschema.Property{
870878
Name: fieldName,
871879
Schema: inferSchemaFromValue(fieldValue),

0 commit comments

Comments
 (0)