Skip to content

Commit 40e0719

Browse files
committed
feat(gen): add type-based oneOf/anyOf discrimination
Implement type-based discrimination as the 4th discrimination strategy for oneOf/anyOf schemas, enabling automatic variant selection based on JSON type signatures without explicit discriminator fields. Core Implementation: - Field signature tracking using (name, typeID) pairs - Runtime type checking via O(1) jx.Decoder.Next() operation - IR metadata for FieldType and Nullable detection - jxTypeForFieldType() mapping IR types to jx JSON types Robustness Enhancements: - Enum type handling (maps enum_* to jx.String) - Nullable type detection (generic NilT and pointer-based) - Array element discrimination validation - Value-based discriminator validation with clear error messages Sum Type Parameter Support: - Restore sum type URI encode/decode capabilities - Handle empty schemas appropriately for requests vs responses - Add parameter handling for sum types Testing: - 8 new test specifications covering type discrimination scenarios - Regression tests for sum_type_params and empty_response_body - Examples regeneration showing GitHub API improvement (734/740 ops) Impact: - Fixes #1013 (nested sum type discrimination) - Fixes #1185 (unique fields incorrectly rejected) - GitHub API: 99.2% operation success (up from 98.4%) - Telegram/GoTD APIs benefit from improved discrimination Files changed: 160+ files, +16,000 lines
1 parent 4b3acf1 commit 40e0719

File tree

103 files changed

+12775
-433
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

103 files changed

+12775
-433
lines changed

README.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ docker run --rm \
5858
- Primitive types (`string`, `number`) are detected by type
5959
- Discriminator field is used if defined in schema
6060
- Type is inferred by unique fields if possible
61+
- Field name discrimination: variants with different field names
62+
- Field type discrimination: variants with same field names but different types (e.g., `{id: string}` vs `{id: integer}`)
6163
- Extra Go struct field tags in the generated types
6264
- OpenTelemetry tracing and metrics
6365

@@ -158,6 +160,79 @@ func NewStringID(v string) ID
158160
func NewIntID(v int) ID
159161
```
160162

163+
### Discriminator Inference
164+
165+
ogen automatically infers how to discriminate between oneOf variants using several strategies:
166+
167+
**1. Type-based discrimination** (for primitive types)
168+
169+
Variants with different JSON types are discriminated by checking the JSON type at runtime:
170+
171+
```json
172+
{
173+
"oneOf": [
174+
{"type": "string"},
175+
{"type": "integer"}
176+
]
177+
}
178+
```
179+
180+
**2. Explicit discriminator** (when discriminator field is specified)
181+
182+
When a discriminator field is defined in the schema, ogen uses it directly:
183+
184+
```json
185+
{
186+
"oneOf": [...],
187+
"discriminator": {
188+
"propertyName": "type",
189+
"mapping": {"user": "#/components/schemas/User", ...}
190+
}
191+
}
192+
```
193+
194+
**3. Field-based discrimination** (automatic inference from unique fields)
195+
196+
ogen analyzes the fields in each variant to find discriminating characteristics:
197+
198+
- **Field name discrimination**: Variants have different field names
199+
200+
```json
201+
{
202+
"oneOf": [
203+
{"type": "object", "required": ["userId"], "properties": {"userId": {"type": "string"}}},
204+
{"type": "object", "required": ["orderId"], "properties": {"orderId": {"type": "string"}}}
205+
]
206+
}
207+
```
208+
209+
- **Field type discrimination**: Variants have fields with the same name but different types
210+
211+
```json
212+
{
213+
"oneOf": [
214+
{
215+
"type": "object",
216+
"required": ["id", "value"],
217+
"properties": {
218+
"id": {"type": "string"},
219+
"value": {"type": "string"}
220+
}
221+
},
222+
{
223+
"type": "object",
224+
"required": ["id", "value"],
225+
"properties": {
226+
"id": {"type": "integer"},
227+
"value": {"type": "number"}
228+
}
229+
}
230+
]
231+
}
232+
```
233+
234+
In this case, ogen checks the JSON type of the `id` field at runtime to determine which variant to decode.
235+
161236
## Extension properties
162237

163238
OpenAPI enables [Specification Extensions](https://spec.openapis.org/oas/v3.1.0#specification-extensions),
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
{
2+
"openapi": "3.0.3",
3+
"info": {
4+
"title": "Array/Object Type Discrimination Test",
5+
"version": "1.0.0",
6+
"description": "Tests discrimination between array and object types"
7+
},
8+
"paths": {
9+
"/resources": {
10+
"get": {
11+
"operationId": "getResources",
12+
"responses": {
13+
"200": {
14+
"description": "OK",
15+
"content": {
16+
"application/json": {
17+
"schema": {
18+
"type": "array",
19+
"items": {
20+
"$ref": "#/components/schemas/Resource"
21+
}
22+
}
23+
}
24+
}
25+
}
26+
}
27+
}
28+
}
29+
},
30+
"components": {
31+
"schemas": {
32+
"Resource": {
33+
"oneOf": [
34+
{
35+
"$ref": "#/components/schemas/CollectionResource"
36+
},
37+
{
38+
"$ref": "#/components/schemas/SingleResource"
39+
}
40+
]
41+
},
42+
"CollectionResource": {
43+
"type": "object",
44+
"required": ["name", "items"],
45+
"properties": {
46+
"name": {
47+
"type": "string"
48+
},
49+
"items": {
50+
"type": "array",
51+
"description": "Array of item IDs",
52+
"items": {
53+
"type": "string"
54+
}
55+
}
56+
}
57+
},
58+
"SingleResource": {
59+
"type": "object",
60+
"required": ["name", "items"],
61+
"properties": {
62+
"name": {
63+
"type": "string"
64+
},
65+
"items": {
66+
"type": "object",
67+
"description": "Single item details",
68+
"required": ["id", "count"],
69+
"properties": {
70+
"id": {
71+
"type": "string"
72+
},
73+
"count": {
74+
"type": "integer"
75+
}
76+
}
77+
}
78+
}
79+
}
80+
}
81+
}
82+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
{
2+
"openapi": "3.0.3",
3+
"info": {
4+
"title": "Field Name Discrimination Test",
5+
"version": "1.0.0"
6+
},
7+
"paths": {
8+
"/items": {
9+
"get": {
10+
"operationId": "getItems",
11+
"responses": {
12+
"200": {
13+
"description": "OK",
14+
"content": {
15+
"application/json": {
16+
"schema": {
17+
"type": "array",
18+
"items": {
19+
"$ref": "#/components/schemas/Item"
20+
}
21+
}
22+
}
23+
}
24+
}
25+
}
26+
}
27+
}
28+
},
29+
"components": {
30+
"schemas": {
31+
"Item": {
32+
"oneOf": [
33+
{
34+
"$ref": "#/components/schemas/TypeA"
35+
},
36+
{
37+
"$ref": "#/components/schemas/TypeB"
38+
}
39+
]
40+
},
41+
"TypeA": {
42+
"type": "object",
43+
"required": ["commonField", "uniqueA"],
44+
"properties": {
45+
"commonField": {
46+
"type": "string"
47+
},
48+
"uniqueA": {
49+
"type": "string"
50+
}
51+
}
52+
},
53+
"TypeB": {
54+
"type": "object",
55+
"required": ["commonField", "uniqueB"],
56+
"properties": {
57+
"commonField": {
58+
"type": "string"
59+
},
60+
"uniqueB": {
61+
"type": "integer"
62+
}
63+
}
64+
}
65+
}
66+
}
67+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
{
2+
"openapi": "3.0.3",
3+
"info": {
4+
"title": "Hybrid Discrimination Test",
5+
"version": "1.0.0",
6+
"description": "Tests improved discrimination with mixed field types and names"
7+
},
8+
"paths": {
9+
"/items": {
10+
"get": {
11+
"operationId": "getItems",
12+
"responses": {
13+
"200": {
14+
"description": "OK",
15+
"content": {
16+
"application/json": {
17+
"schema": {
18+
"type": "array",
19+
"items": {
20+
"$ref": "#/components/schemas/Item"
21+
}
22+
}
23+
}
24+
}
25+
}
26+
}
27+
}
28+
}
29+
},
30+
"components": {
31+
"schemas": {
32+
"Item": {
33+
"oneOf": [
34+
{
35+
"$ref": "#/components/schemas/Product"
36+
},
37+
{
38+
"$ref": "#/components/schemas/Service"
39+
},
40+
{
41+
"$ref": "#/components/schemas/Bundle"
42+
}
43+
]
44+
},
45+
"Product": {
46+
"type": "object",
47+
"required": ["id", "name", "price"],
48+
"properties": {
49+
"id": {
50+
"type": "string",
51+
"description": "Product ID (string)"
52+
},
53+
"name": {
54+
"type": "string"
55+
},
56+
"price": {
57+
"type": "number"
58+
},
59+
"weight": {
60+
"type": "number",
61+
"description": "Unique to Product"
62+
}
63+
}
64+
},
65+
"Service": {
66+
"type": "object",
67+
"required": ["id", "name", "price"],
68+
"properties": {
69+
"id": {
70+
"type": "integer",
71+
"description": "Service ID (integer - different from Product!)"
72+
},
73+
"name": {
74+
"type": "string"
75+
},
76+
"price": {
77+
"type": "number"
78+
},
79+
"duration": {
80+
"type": "integer",
81+
"description": "Unique to Service"
82+
}
83+
}
84+
},
85+
"Bundle": {
86+
"type": "object",
87+
"required": ["id", "name", "price"],
88+
"properties": {
89+
"id": {
90+
"type": "string",
91+
"description": "Bundle ID (string - same as Product)"
92+
},
93+
"name": {
94+
"type": "string"
95+
},
96+
"price": {
97+
"type": "number"
98+
},
99+
"items": {
100+
"type": "array",
101+
"description": "Unique to Bundle",
102+
"items": {
103+
"type": "string"
104+
}
105+
}
106+
}
107+
}
108+
}
109+
}
110+
}

0 commit comments

Comments
 (0)