Skip to content

Commit 14bb9c7

Browse files
authored
Merge pull request #1588 from lanej/feature/value-based-discrimination
feat(gen): add value-based oneOf/anyOf discrimination
2 parents cb69a49 + b6d3cbe commit 14bb9c7

39 files changed

+10986
-53
lines changed

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ docker run --rm \
6060
- Type is inferred by unique fields if possible
6161
- Field name discrimination: variants with different field names
6262
- Field type discrimination: variants with same field names but different types (e.g., `{id: string}` vs `{id: integer}`)
63+
- Field value discrimination: variants with same field names and types but different enum values
6364
- Extra Go struct field tags in the generated types
6465
- OpenTelemetry tracing and metrics
6566

@@ -233,6 +234,31 @@ ogen analyzes the fields in each variant to find discriminating characteristics:
233234

234235
In this case, ogen checks the JSON type of the `id` field at runtime to determine which variant to decode.
235236

237+
- **Field value discrimination**: Variants have fields with the same name and type but different enum values
238+
239+
```json
240+
{
241+
"oneOf": [
242+
{
243+
"type": "object",
244+
"required": ["status"],
245+
"properties": {
246+
"status": {"type": "string", "enum": ["active", "pending"]}
247+
}
248+
},
249+
{
250+
"type": "object",
251+
"required": ["status"],
252+
"properties": {
253+
"status": {"type": "string", "enum": ["inactive", "deleted"]}
254+
}
255+
}
256+
]
257+
}
258+
```
259+
260+
In this case, ogen checks the actual string value of the `status` field at runtime and matches it against each variant's enum values. The enum values must be disjoint (non-overlapping) for this to work. If enum values overlap, ogen will report an error and suggest using an explicit discriminator.
261+
236262
## Extension properties
237263

238264
OpenAPI enables [Specification Extensions](https://spec.openapis.org/oas/v3.1.0#specification-extensions),
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
{
2+
"openapi": "3.0.3",
3+
"info": {
4+
"title": "Value-Based Discrimination Overlap Test",
5+
"version": "1.0.0",
6+
"description": "Tests that overlapping enum values are rejected - this should fail"
7+
},
8+
"paths": {
9+
"/status": {
10+
"get": {
11+
"operationId": "getStatus",
12+
"responses": {
13+
"200": {
14+
"description": "Status response",
15+
"content": {
16+
"application/json": {
17+
"schema": {
18+
"$ref": "#/components/schemas/StatusResponse"
19+
}
20+
}
21+
}
22+
}
23+
}
24+
}
25+
}
26+
},
27+
"components": {
28+
"schemas": {
29+
"StatusResponse": {
30+
"description": "Overlapping enum values - should fail discrimination",
31+
"oneOf": [
32+
{
33+
"$ref": "#/components/schemas/StatusA"
34+
},
35+
{
36+
"$ref": "#/components/schemas/StatusB"
37+
}
38+
]
39+
},
40+
"StatusA": {
41+
"type": "object",
42+
"required": ["status"],
43+
"properties": {
44+
"status": {
45+
"type": "string",
46+
"enum": ["active", "pending", "inactive"]
47+
},
48+
"fieldA": {
49+
"type": "string"
50+
}
51+
}
52+
},
53+
"StatusB": {
54+
"type": "object",
55+
"required": ["status"],
56+
"properties": {
57+
"status": {
58+
"type": "string",
59+
"enum": ["inactive", "deleted", "archived"]
60+
},
61+
"fieldB": {
62+
"type": "string"
63+
}
64+
}
65+
}
66+
}
67+
}
68+
}
Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
{
2+
"openapi": "3.0.3",
3+
"info": {
4+
"title": "Value-Based Discrimination Test",
5+
"version": "1.0.0",
6+
"description": "Tests that oneOf variants can be discriminated by enum values (same field name and JSON type, but different enum values)"
7+
},
8+
"paths": {
9+
"/status": {
10+
"get": {
11+
"operationId": "getStatus",
12+
"responses": {
13+
"200": {
14+
"description": "Status response",
15+
"content": {
16+
"application/json": {
17+
"schema": {
18+
"$ref": "#/components/schemas/StatusResponse"
19+
}
20+
}
21+
}
22+
}
23+
}
24+
}
25+
},
26+
"/resource": {
27+
"get": {
28+
"operationId": "getResource",
29+
"responses": {
30+
"200": {
31+
"description": "Resource response",
32+
"content": {
33+
"application/json": {
34+
"schema": {
35+
"$ref": "#/components/schemas/Resource"
36+
}
37+
}
38+
}
39+
}
40+
}
41+
}
42+
},
43+
"/event": {
44+
"post": {
45+
"operationId": "logEvent",
46+
"requestBody": {
47+
"required": true,
48+
"content": {
49+
"application/json": {
50+
"schema": {
51+
"$ref": "#/components/schemas/Event"
52+
}
53+
}
54+
}
55+
},
56+
"responses": {
57+
"200": {
58+
"description": "OK"
59+
}
60+
}
61+
}
62+
},
63+
"/shipping-option": {
64+
"get": {
65+
"operationId": "getShippingOption",
66+
"description": "Test overlapping enum values with a discriminating field - should use carrier field to discriminate",
67+
"responses": {
68+
"200": {
69+
"description": "Shipping option response",
70+
"content": {
71+
"application/json": {
72+
"schema": {
73+
"$ref": "#/components/schemas/ShippingOption"
74+
}
75+
}
76+
}
77+
}
78+
}
79+
}
80+
}
81+
},
82+
"components": {
83+
"schemas": {
84+
"StatusResponse": {
85+
"description": "Basic enum value discrimination - different enum values for same field",
86+
"oneOf": [
87+
{
88+
"$ref": "#/components/schemas/ActiveStatus"
89+
},
90+
{
91+
"$ref": "#/components/schemas/InactiveStatus"
92+
}
93+
]
94+
},
95+
"ActiveStatus": {
96+
"type": "object",
97+
"required": ["status", "lastActive"],
98+
"properties": {
99+
"status": {
100+
"type": "string",
101+
"enum": ["active", "pending"],
102+
"description": "Active states"
103+
},
104+
"lastActive": {
105+
"type": "string",
106+
"format": "date-time"
107+
}
108+
}
109+
},
110+
"InactiveStatus": {
111+
"type": "object",
112+
"required": ["status", "deletedAt"],
113+
"properties": {
114+
"status": {
115+
"type": "string",
116+
"enum": ["inactive", "deleted"],
117+
"description": "Inactive states"
118+
},
119+
"deletedAt": {
120+
"type": "string",
121+
"format": "date-time"
122+
}
123+
}
124+
},
125+
"Resource": {
126+
"description": "Multiple fields with value-based discrimination",
127+
"oneOf": [
128+
{
129+
"$ref": "#/components/schemas/UserResource"
130+
},
131+
{
132+
"$ref": "#/components/schemas/AdminResource"
133+
}
134+
]
135+
},
136+
"UserResource": {
137+
"type": "object",
138+
"required": ["type", "role", "userId"],
139+
"properties": {
140+
"type": {
141+
"type": "string",
142+
"enum": ["user"],
143+
"description": "Resource type"
144+
},
145+
"role": {
146+
"type": "string",
147+
"enum": ["viewer", "editor"],
148+
"description": "User roles"
149+
},
150+
"userId": {
151+
"type": "string"
152+
}
153+
}
154+
},
155+
"AdminResource": {
156+
"type": "object",
157+
"required": ["type", "role", "adminId"],
158+
"properties": {
159+
"type": {
160+
"type": "string",
161+
"enum": ["admin"],
162+
"description": "Resource type"
163+
},
164+
"role": {
165+
"type": "string",
166+
"enum": ["superadmin", "moderator"],
167+
"description": "Admin roles"
168+
},
169+
"adminId": {
170+
"type": "string"
171+
}
172+
}
173+
},
174+
"Event": {
175+
"description": "Mixed discrimination - some fields by type, some by value",
176+
"oneOf": [
177+
{
178+
"$ref": "#/components/schemas/UserEvent"
179+
},
180+
{
181+
"$ref": "#/components/schemas/SystemEvent"
182+
},
183+
{
184+
"$ref": "#/components/schemas/MetricEvent"
185+
}
186+
]
187+
},
188+
"UserEvent": {
189+
"type": "object",
190+
"required": ["eventType", "priority", "userId"],
191+
"properties": {
192+
"eventType": {
193+
"type": "string",
194+
"enum": ["user_login", "user_logout"],
195+
"description": "User event types - discriminated by value"
196+
},
197+
"priority": {
198+
"type": "string",
199+
"description": "String priority - discriminated by type"
200+
},
201+
"userId": {
202+
"type": "string",
203+
"description": "Unique field - discriminated by name"
204+
}
205+
}
206+
},
207+
"SystemEvent": {
208+
"type": "object",
209+
"required": ["eventType", "priority", "systemId"],
210+
"properties": {
211+
"eventType": {
212+
"type": "string",
213+
"enum": ["system_start", "system_stop"],
214+
"description": "System event types - discriminated by value"
215+
},
216+
"priority": {
217+
"type": "integer",
218+
"description": "Integer priority - discriminated by type"
219+
},
220+
"systemId": {
221+
"type": "string",
222+
"description": "Unique field - discriminated by name"
223+
}
224+
}
225+
},
226+
"MetricEvent": {
227+
"type": "object",
228+
"required": ["eventType", "priority", "metricId"],
229+
"properties": {
230+
"eventType": {
231+
"type": "string",
232+
"enum": ["metric_update", "metric_alert"],
233+
"description": "Metric event types - discriminated by value"
234+
},
235+
"priority": {
236+
"type": "number",
237+
"description": "Numeric priority - discriminated by type"
238+
},
239+
"metricId": {
240+
"type": "string",
241+
"description": "Unique field - discriminated by name"
242+
}
243+
}
244+
},
245+
"ShippingOption": {
246+
"description": "Overlapping enum values with a discriminating field - carrier field discriminates, signature has overlapping values",
247+
"oneOf": [
248+
{
249+
"$ref": "#/components/schemas/USPSShippingOption"
250+
},
251+
{
252+
"$ref": "#/components/schemas/FedExShippingOption"
253+
}
254+
]
255+
},
256+
"USPSShippingOption": {
257+
"type": "object",
258+
"required": ["carrier", "signature"],
259+
"properties": {
260+
"carrier": {
261+
"type": "string",
262+
"enum": ["usps"],
263+
"description": "Carrier identifier - unique enum value for discrimination"
264+
},
265+
"signature": {
266+
"type": "string",
267+
"enum": ["gift", "sample", "standard"],
268+
"description": "Signature type - has overlapping values with FedEx"
269+
}
270+
}
271+
},
272+
"FedExShippingOption": {
273+
"type": "object",
274+
"required": ["carrier", "signature"],
275+
"properties": {
276+
"carrier": {
277+
"type": "string",
278+
"enum": ["fedex"],
279+
"description": "Carrier identifier - unique enum value for discrimination"
280+
},
281+
"signature": {
282+
"type": "string",
283+
"enum": ["gift", "sample", "express"],
284+
"description": "Signature type - has overlapping values with USPS"
285+
}
286+
}
287+
}
288+
}
289+
}
290+
}

0 commit comments

Comments
 (0)