Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2756,6 +2756,13 @@ protected void updateModelForComposedSchema(CodegenModel m, Schema schema, Map<S

// interfaces (schemas defined in allOf, anyOf, oneOf)
List<Schema> interfaces = ModelUtils.getInterfaces(composed);

// For anyOf/oneOf, required fields should be the intersection across members,
// not the union. A field is only guaranteed present if ALL members require it.
boolean isAnyOfOrOneOf = (composed.getAnyOf() != null && !composed.getAnyOf().isEmpty())
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use ModelUtil.hasOneOF(composed) || ModelUtils.hasAnyOf(composed)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in cf5b0df, thanks.

|| (composed.getOneOf() != null && !composed.getOneOf().isEmpty());
List<Set<String>> perMemberRequiredSets = isAnyOfOrOneOf ? new ArrayList<>() : null;

if (!interfaces.isEmpty()) {
// m.interfaces is for backward compatibility
if (m.interfaces == null)
Expand Down Expand Up @@ -2816,7 +2823,14 @@ protected void updateModelForComposedSchema(CodegenModel m, Schema schema, Map<S
} else {
// composition
Map<String, Schema> newProperties = new LinkedHashMap<>();
addProperties(newProperties, required, refSchema, new HashSet<>());
if (isAnyOfOrOneOf) {
// Collect required fields per-member for later intersection
List<String> memberRequired = new ArrayList<>();
addProperties(newProperties, memberRequired, refSchema, new HashSet<>());
perMemberRequiredSets.add(new HashSet<>(memberRequired));
} else {
addProperties(newProperties, required, refSchema, new HashSet<>());
}
mergeProperties(properties, newProperties);
addProperties(allProperties, allRequired, refSchema, new HashSet<>());
}
Expand Down Expand Up @@ -2857,8 +2871,15 @@ protected void updateModelForComposedSchema(CodegenModel m, Schema schema, Map<S
for (Schema component : interfaces) {
if (component.get$ref() == null) {
if (component != null) {
// component is the child schema
addProperties(properties, required, component, new HashSet<>());
if (isAnyOfOrOneOf) {
// Collect required fields per-member for later intersection
List<String> memberRequired = new ArrayList<>();
addProperties(properties, memberRequired, component, new HashSet<>());
perMemberRequiredSets.add(new HashSet<>(memberRequired));
} else {
// component is the child schema
addProperties(properties, required, component, new HashSet<>());
}

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

// For anyOf/oneOf, compute the intersection of required fields across all members.
// A field is only required in the merged model if ALL members require it.
if (isAnyOfOrOneOf && !perMemberRequiredSets.isEmpty()) {
Set<String> intersected = new HashSet<>(perMemberRequiredSets.get(0));
for (int i = 1; i < perMemberRequiredSets.size(); i++) {
intersected.retainAll(perMemberRequiredSets.get(i));
}
required.addAll(intersected);
}

if (composed.getRequired() != null) {
required.addAll(composed.getRequired());
allRequired.addAll(composed.getRequired());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@
import org.openapitools.codegen.DefaultCodegen;
import org.openapitools.codegen.TestUtils;
import org.openapitools.codegen.languages.Swift6ClientCodegen;
import org.openapitools.codegen.utils.ModelUtils;
import org.testng.Assert;
import org.testng.annotations.Test;

import java.util.Map;

@SuppressWarnings("static-method")
public class Swift6ClientCodegenModelTest {

Expand Down Expand Up @@ -163,4 +166,74 @@ public void useCustomDateTimeTest() {
Assert.assertFalse(property7.isContainer);
}

@Test(description = "anyOf with different required fields should use intersection for required", enabled = true)
public void anyOfRequiredFieldsIntersectionTest() {
// VoteResponse requires: status, voteId
// APIError requires: status, reason, code
// Intersection: only "status" should be required in the merged model
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/swift6_anyof_required.yaml");
final Swift6ClientCodegen codegen = new Swift6ClientCodegen();
codegen.setOpenAPI(openAPI);
codegen.processOpts();

// The inline model resolver creates a model for the anyOf response.
// Find the composed schema that merges VoteResponse and APIError.
Map<String, Schema> schemas = ModelUtils.getSchemas(openAPI);
Schema composedSchema = null;
String composedName = null;
for (Map.Entry<String, Schema> entry : schemas.entrySet()) {
Schema s = entry.getValue();
if (s.getAnyOf() != null && !s.getAnyOf().isEmpty()) {
composedSchema = s;
composedName = entry.getKey();
break;
}
}
Assert.assertNotNull(composedSchema, "Should find an anyOf composed schema");

final CodegenModel cm = codegen.fromModel(composedName, composedSchema);

// "status" is required in BOTH VoteResponse and APIError -> should be required
CodegenProperty statusProp = cm.vars.stream()
.filter(p -> p.baseName.equals("status"))
.findFirst().orElse(null);
Assert.assertNotNull(statusProp, "status property should exist");
Assert.assertTrue(statusProp.required, "status should be required (present in all anyOf members)");

// "voteId" is required only in VoteResponse, not in APIError -> should NOT be required
CodegenProperty voteIdProp = cm.vars.stream()
.filter(p -> p.baseName.equals("voteId"))
.findFirst().orElse(null);
Assert.assertNotNull(voteIdProp, "voteId property should exist");
Assert.assertFalse(voteIdProp.required, "voteId should NOT be required (only in VoteResponse, not APIError)");

// "reason" is required only in APIError, not in VoteResponse -> should NOT be required
CodegenProperty reasonProp = cm.vars.stream()
.filter(p -> p.baseName.equals("reason"))
.findFirst().orElse(null);
Assert.assertNotNull(reasonProp, "reason property should exist");
Assert.assertFalse(reasonProp.required, "reason should NOT be required (only in APIError, not VoteResponse)");

// "code" is required only in APIError, not in VoteResponse -> should NOT be required
CodegenProperty codeProp = cm.vars.stream()
.filter(p -> p.baseName.equals("code"))
.findFirst().orElse(null);
Assert.assertNotNull(codeProp, "code property should exist");
Assert.assertFalse(codeProp.required, "code should NOT be required (only in APIError, not VoteResponse)");

// "isVerified" is optional in VoteResponse, not in APIError -> should NOT be required
CodegenProperty isVerifiedProp = cm.vars.stream()
.filter(p -> p.baseName.equals("isVerified"))
.findFirst().orElse(null);
Assert.assertNotNull(isVerifiedProp, "isVerified property should exist");
Assert.assertFalse(isVerifiedProp.required, "isVerified should NOT be required");

// "secondaryCode" is optional in APIError -> should NOT be required
CodegenProperty secondaryCodeProp = cm.vars.stream()
.filter(p -> p.baseName.equals("secondaryCode"))
.findFirst().orElse(null);
Assert.assertNotNull(secondaryCodeProp, "secondaryCode property should exist");
Assert.assertFalse(secondaryCodeProp.required, "secondaryCode should NOT be required");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
openapi: 3.0.0
info:
title: anyOf Required Fields Test
version: 1.0.0
paths:
/vote:
post:
operationId: voteOnItem
responses:
'200':
description: Success or error response
content:
application/json:
schema:
anyOf:
- $ref: '#/components/schemas/VoteResponse'
- $ref: '#/components/schemas/APIError'
components:
schemas:
APIStatus:
type: string
enum:
- success
- failed
VoteResponse:
type: object
required:
- status
- voteId
properties:
status:
$ref: '#/components/schemas/APIStatus'
voteId:
type: string
isVerified:
type: boolean
APIError:
type: object
required:
- status
- reason
- code
properties:
status:
$ref: '#/components/schemas/APIStatus'
reason:
type: string
code:
type: string
secondaryCode:
type: string
Loading