diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java index ad002afbcd33..98af7c3b671f 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java @@ -2382,6 +2382,15 @@ public static boolean isNullTypeSchema(OpenAPI openAPI, Schema schema) { return false; } + // OpenAPI 3.0.x: nullable object with no properties or constraints expresses nullability + if (!(schema instanceof JsonSchema) // 3.0.x only + && "object".equals(schema.getType()) + && Boolean.TRUE.equals(schema.getNullable()) + && schema.get$ref() == null + && schema.getAdditionalProperties() == null) { + return true; + } + // convert referenced enum of null only to `nullable:true` if (schema.getEnum() != null && schema.getEnum().size() == 1) { if ("null".equals(String.valueOf(schema.getEnum().get(0)))) { diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java index 69fb7de07ada..c1e2e88f6715 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java @@ -4405,6 +4405,19 @@ public void testJspecify(String library, boolean useSpringBoot4, boolean hasJspe } + @Test(description = "anyOf with $ref and {type: object, nullable: true} should resolve to typed nullable field, not Object") + public void testAnyOfBareNullableObjectResolvesToTypedField() { + Map files = generateFromContract( + "src/test/resources/bugs/issue_anyof_bare_nullable_object.yaml", + JavaClientCodegen.JERSEY3); + + JavaFileAssert.assertThat(files.get("Order.java")) + .fileContains("Address") + .fileDoesNotContain("OrderShippingAddress", "Object getShippingAddress"); + + Assert.assertNull(files.get("OrderShippingAddress.java"), + "Should not generate synthetic anyOf wrapper; the anyOf should simplify to Address"); + } @DataProvider(name = "replaceOneOf") public Object[][] replaceOneOf() { return new Object[][]{ diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/utils/ModelUtilsTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/utils/ModelUtilsTest.java index 906ace829bcb..7397b8874121 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/utils/ModelUtilsTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/utils/ModelUtilsTest.java @@ -654,6 +654,39 @@ public void isNullTypeSchemaTest() { schema = openAPI.getComponents().getSchemas().get("JustDescription"); assertFalse(ModelUtils.isNullTypeSchema(openAPI, schema)); + + // {type: "object", nullable: true} with no properties/constraints expresses nullability (OAS 3.0.3) + schema = openAPI.getComponents().getSchemas().get("BareNullableObject"); + assertTrue(ModelUtils.isNullTypeSchema(openAPI, schema)); + + // {type: "object", nullable: true} WITH properties is a real object, not expressing nullability + schema = openAPI.getComponents().getSchemas().get("NullableObjectWithProperties"); + assertFalse(ModelUtils.isNullTypeSchema(openAPI, schema)); + + // {type: "object", nullable: true, additionalProperties: ...} is a nullable map, not expressing nullability + schema = openAPI.getComponents().getSchemas().get("NullableObjectMap"); + assertFalse(ModelUtils.isNullTypeSchema(openAPI, schema)); + } + + @Test + public void isNullTypeSchemaTestBareNullableObject() { + OpenAPI openAPI = TestUtils.parseSpec( + "src/test/resources/bugs/issue_anyof_bare_nullable_object.yaml"); + Schema order = (Schema) openAPI.getComponents().getSchemas().get("Order"); + Schema shippingProp = (Schema) order.getProperties().get("shippingAddress"); + assertNotNull(shippingProp.getAnyOf(), "shippingAddress should have anyOf"); + + List anyOf = shippingProp.getAnyOf(); + assertEquals(anyOf.size(), 2); + + // first sub-schema is the $ref to Address + Schema refSchema = anyOf.get(0); + assertFalse(ModelUtils.isNullTypeSchema(openAPI, refSchema)); + + // second sub-schema is {type: object, nullable: true} — expresses nullability + Schema bareNullableObject = anyOf.get(1); + assertTrue(ModelUtils.isNullTypeSchema(openAPI, bareNullableObject), + "{type: object, nullable: true} with no properties/constraints expresses nullability"); } @Test @@ -695,6 +728,11 @@ public void isNullTypeSchemaTestWith31Spec() { schema = openAPI.getComponents().getSchemas().get("JustDescription"); assertFalse(ModelUtils.isNullTypeSchema(openAPI, schema)); + + // In 3.1, {type: object, nullable: true} is NOT a null type — it's a real + // nullable object. Nullability in 3.1 is expressed via type: ["object", "null"]. + schema = openAPI.getComponents().getSchemas().get("BareNullableObject"); + assertFalse(ModelUtils.isNullTypeSchema(openAPI, schema)); } @Test diff --git a/modules/openapi-generator/src/test/resources/3_0/null_schema_test.yaml b/modules/openapi-generator/src/test/resources/3_0/null_schema_test.yaml index e403eeef1397..ff3fe942f4ee 100644 --- a/modules/openapi-generator/src/test/resources/3_0/null_schema_test.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/null_schema_test.yaml @@ -104,4 +104,18 @@ components: - $ref: '#/components/schemas/IntegerRef' - $ref: '#/components/schemas/StringRef' JustDescription: - description: A schema with just description \ No newline at end of file + description: A schema with just description + BareNullableObject: + type: object + nullable: true + NullableObjectWithProperties: + type: object + nullable: true + properties: + name: + type: string + NullableObjectMap: + type: object + nullable: true + additionalProperties: + type: string \ No newline at end of file diff --git a/modules/openapi-generator/src/test/resources/3_1/null_schema_test.yaml b/modules/openapi-generator/src/test/resources/3_1/null_schema_test.yaml index 1a3de1a10721..b1306c0a6dfd 100644 --- a/modules/openapi-generator/src/test/resources/3_1/null_schema_test.yaml +++ b/modules/openapi-generator/src/test/resources/3_1/null_schema_test.yaml @@ -105,3 +105,6 @@ components: - $ref: '#/components/schemas/StringRef' JustDescription: description: A schema with just description + BareNullableObject: + type: object + nullable: true diff --git a/modules/openapi-generator/src/test/resources/bugs/issue_anyof_bare_nullable_object.yaml b/modules/openapi-generator/src/test/resources/bugs/issue_anyof_bare_nullable_object.yaml new file mode 100644 index 000000000000..8866dcf13845 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/bugs/issue_anyof_bare_nullable_object.yaml @@ -0,0 +1,44 @@ +openapi: 3.0.3 +info: + title: anyOf bare nullable object test + description: > + Tests that anyOf with a $ref and {type: object, nullable: true} (no properties) + is simplified to a typed nullable field, not Object. + This pattern is produced by apispec 6.7.1+ for OpenAPI 3.0.x specs. + version: 1.0.0 +paths: + /orders/{orderId}: + get: + operationId: getOrder + parameters: + - name: orderId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Order' +components: + schemas: + Order: + type: object + properties: + id: + type: string + shippingAddress: + anyOf: + - $ref: '#/components/schemas/Address' + - type: object + nullable: true + Address: + type: object + properties: + street: + type: string + city: + type: string