Skip to content

Commit 13f52c1

Browse files
committed
[BUG][CORE] Use intersection of required fields across anyOf/oneOf members
The merged composed model from anyOf/oneOf added every member's required fields to the resulting `required` set. Per OAS / JSON Schema semantics, a value is only guaranteed present if EVERY member of an anyOf/oneOf requires it, so the merged set should be the intersection. Example: response: anyOf: - $ref: '#/components/schemas/VoteResponse' # required: [status, voteId] - $ref: '#/components/schemas/APIError' # required: [status, reason, code] Previously: status, voteId, reason, code all marked required. Now: only status is required (the only field present in both). allOf semantics are unchanged (still union). Affects every generator that consumes CodegenModel.required for non-null types or constructor signatures (Swift, Kotlin, Java POJOs, Dart, etc.). Sample regeneration may flip previously-required fields to optional in generated code where this pattern is in use; that is the intended behavior. Test: anyOfRequiredFieldsIntersectionTest in Swift6ClientCodegenModelTest covers the contract via swift6_anyof_required.yaml (chosen because swift6 is one of the affected generators; the change itself lives in DefaultCodegen and applies everywhere). Closes #23667
1 parent 2917ce8 commit 13f52c1

3 files changed

Lines changed: 158 additions & 3 deletions

File tree

modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2756,6 +2756,13 @@ protected void updateModelForComposedSchema(CodegenModel m, Schema schema, Map<S
27562756

27572757
// interfaces (schemas defined in allOf, anyOf, oneOf)
27582758
List<Schema> interfaces = ModelUtils.getInterfaces(composed);
2759+
2760+
// For anyOf/oneOf, required fields should be the intersection across members,
2761+
// not the union. A field is only guaranteed present if ALL members require it.
2762+
boolean isAnyOfOrOneOf = (composed.getAnyOf() != null && !composed.getAnyOf().isEmpty())
2763+
|| (composed.getOneOf() != null && !composed.getOneOf().isEmpty());
2764+
List<Set<String>> perMemberRequiredSets = isAnyOfOrOneOf ? new ArrayList<>() : null;
2765+
27592766
if (!interfaces.isEmpty()) {
27602767
// m.interfaces is for backward compatibility
27612768
if (m.interfaces == null)
@@ -2816,7 +2823,14 @@ protected void updateModelForComposedSchema(CodegenModel m, Schema schema, Map<S
28162823
} else {
28172824
// composition
28182825
Map<String, Schema> newProperties = new LinkedHashMap<>();
2819-
addProperties(newProperties, required, refSchema, new HashSet<>());
2826+
if (isAnyOfOrOneOf) {
2827+
// Collect required fields per-member for later intersection
2828+
List<String> memberRequired = new ArrayList<>();
2829+
addProperties(newProperties, memberRequired, refSchema, new HashSet<>());
2830+
perMemberRequiredSets.add(new HashSet<>(memberRequired));
2831+
} else {
2832+
addProperties(newProperties, required, refSchema, new HashSet<>());
2833+
}
28202834
mergeProperties(properties, newProperties);
28212835
addProperties(allProperties, allRequired, refSchema, new HashSet<>());
28222836
}
@@ -2857,8 +2871,15 @@ protected void updateModelForComposedSchema(CodegenModel m, Schema schema, Map<S
28572871
for (Schema component : interfaces) {
28582872
if (component.get$ref() == null) {
28592873
if (component != null) {
2860-
// component is the child schema
2861-
addProperties(properties, required, component, new HashSet<>());
2874+
if (isAnyOfOrOneOf) {
2875+
// Collect required fields per-member for later intersection
2876+
List<String> memberRequired = new ArrayList<>();
2877+
addProperties(properties, memberRequired, component, new HashSet<>());
2878+
perMemberRequiredSets.add(new HashSet<>(memberRequired));
2879+
} else {
2880+
// component is the child schema
2881+
addProperties(properties, required, component, new HashSet<>());
2882+
}
28622883

28632884
// includes child's properties (all, required) in allProperties, allRequired
28642885
addProperties(allProperties, allRequired, component, new HashSet<>());
@@ -2868,6 +2889,16 @@ protected void updateModelForComposedSchema(CodegenModel m, Schema schema, Map<S
28682889
}
28692890
}
28702891

2892+
// For anyOf/oneOf, compute the intersection of required fields across all members.
2893+
// A field is only required in the merged model if ALL members require it.
2894+
if (isAnyOfOrOneOf && !perMemberRequiredSets.isEmpty()) {
2895+
Set<String> intersected = new HashSet<>(perMemberRequiredSets.get(0));
2896+
for (int i = 1; i < perMemberRequiredSets.size(); i++) {
2897+
intersected.retainAll(perMemberRequiredSets.get(i));
2898+
}
2899+
required.addAll(intersected);
2900+
}
2901+
28712902
if (composed.getRequired() != null) {
28722903
required.addAll(composed.getRequired());
28732904
allRequired.addAll(composed.getRequired());

modules/openapi-generator/src/test/java/org/openapitools/codegen/swift6/Swift6ClientCodegenModelTest.java

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,12 @@
2525
import org.openapitools.codegen.DefaultCodegen;
2626
import org.openapitools.codegen.TestUtils;
2727
import org.openapitools.codegen.languages.Swift6ClientCodegen;
28+
import org.openapitools.codegen.utils.ModelUtils;
2829
import org.testng.Assert;
2930
import org.testng.annotations.Test;
3031

32+
import java.util.Map;
33+
3134
@SuppressWarnings("static-method")
3235
public class Swift6ClientCodegenModelTest {
3336

@@ -163,4 +166,74 @@ public void useCustomDateTimeTest() {
163166
Assert.assertFalse(property7.isContainer);
164167
}
165168

169+
@Test(description = "anyOf with different required fields should use intersection for required", enabled = true)
170+
public void anyOfRequiredFieldsIntersectionTest() {
171+
// VoteResponse requires: status, voteId
172+
// APIError requires: status, reason, code
173+
// Intersection: only "status" should be required in the merged model
174+
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/swift6_anyof_required.yaml");
175+
final Swift6ClientCodegen codegen = new Swift6ClientCodegen();
176+
codegen.setOpenAPI(openAPI);
177+
codegen.processOpts();
178+
179+
// The inline model resolver creates a model for the anyOf response.
180+
// Find the composed schema that merges VoteResponse and APIError.
181+
Map<String, Schema> schemas = ModelUtils.getSchemas(openAPI);
182+
Schema composedSchema = null;
183+
String composedName = null;
184+
for (Map.Entry<String, Schema> entry : schemas.entrySet()) {
185+
Schema s = entry.getValue();
186+
if (s.getAnyOf() != null && !s.getAnyOf().isEmpty()) {
187+
composedSchema = s;
188+
composedName = entry.getKey();
189+
break;
190+
}
191+
}
192+
Assert.assertNotNull(composedSchema, "Should find an anyOf composed schema");
193+
194+
final CodegenModel cm = codegen.fromModel(composedName, composedSchema);
195+
196+
// "status" is required in BOTH VoteResponse and APIError -> should be required
197+
CodegenProperty statusProp = cm.vars.stream()
198+
.filter(p -> p.baseName.equals("status"))
199+
.findFirst().orElse(null);
200+
Assert.assertNotNull(statusProp, "status property should exist");
201+
Assert.assertTrue(statusProp.required, "status should be required (present in all anyOf members)");
202+
203+
// "voteId" is required only in VoteResponse, not in APIError -> should NOT be required
204+
CodegenProperty voteIdProp = cm.vars.stream()
205+
.filter(p -> p.baseName.equals("voteId"))
206+
.findFirst().orElse(null);
207+
Assert.assertNotNull(voteIdProp, "voteId property should exist");
208+
Assert.assertFalse(voteIdProp.required, "voteId should NOT be required (only in VoteResponse, not APIError)");
209+
210+
// "reason" is required only in APIError, not in VoteResponse -> should NOT be required
211+
CodegenProperty reasonProp = cm.vars.stream()
212+
.filter(p -> p.baseName.equals("reason"))
213+
.findFirst().orElse(null);
214+
Assert.assertNotNull(reasonProp, "reason property should exist");
215+
Assert.assertFalse(reasonProp.required, "reason should NOT be required (only in APIError, not VoteResponse)");
216+
217+
// "code" is required only in APIError, not in VoteResponse -> should NOT be required
218+
CodegenProperty codeProp = cm.vars.stream()
219+
.filter(p -> p.baseName.equals("code"))
220+
.findFirst().orElse(null);
221+
Assert.assertNotNull(codeProp, "code property should exist");
222+
Assert.assertFalse(codeProp.required, "code should NOT be required (only in APIError, not VoteResponse)");
223+
224+
// "isVerified" is optional in VoteResponse, not in APIError -> should NOT be required
225+
CodegenProperty isVerifiedProp = cm.vars.stream()
226+
.filter(p -> p.baseName.equals("isVerified"))
227+
.findFirst().orElse(null);
228+
Assert.assertNotNull(isVerifiedProp, "isVerified property should exist");
229+
Assert.assertFalse(isVerifiedProp.required, "isVerified should NOT be required");
230+
231+
// "secondaryCode" is optional in APIError -> should NOT be required
232+
CodegenProperty secondaryCodeProp = cm.vars.stream()
233+
.filter(p -> p.baseName.equals("secondaryCode"))
234+
.findFirst().orElse(null);
235+
Assert.assertNotNull(secondaryCodeProp, "secondaryCode property should exist");
236+
Assert.assertFalse(secondaryCodeProp.required, "secondaryCode should NOT be required");
237+
}
238+
166239
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
openapi: 3.0.0
2+
info:
3+
title: anyOf Required Fields Test
4+
version: 1.0.0
5+
paths:
6+
/vote:
7+
post:
8+
operationId: voteOnItem
9+
responses:
10+
'200':
11+
description: Success or error response
12+
content:
13+
application/json:
14+
schema:
15+
anyOf:
16+
- $ref: '#/components/schemas/VoteResponse'
17+
- $ref: '#/components/schemas/APIError'
18+
components:
19+
schemas:
20+
APIStatus:
21+
type: string
22+
enum:
23+
- success
24+
- failed
25+
VoteResponse:
26+
type: object
27+
required:
28+
- status
29+
- voteId
30+
properties:
31+
status:
32+
$ref: '#/components/schemas/APIStatus'
33+
voteId:
34+
type: string
35+
isVerified:
36+
type: boolean
37+
APIError:
38+
type: object
39+
required:
40+
- status
41+
- reason
42+
- code
43+
properties:
44+
status:
45+
$ref: '#/components/schemas/APIStatus'
46+
reason:
47+
type: string
48+
code:
49+
type: string
50+
secondaryCode:
51+
type: string

0 commit comments

Comments
 (0)