Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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 @@ -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())
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
&& 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)))) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, File> 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[][]{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Schema> 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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,18 @@ components:
- $ref: '#/components/schemas/IntegerRef'
- $ref: '#/components/schemas/StringRef'
JustDescription:
description: A schema with just description
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
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,6 @@ components:
- $ref: '#/components/schemas/StringRef'
JustDescription:
description: A schema with just description
BareNullableObject:
type: object
nullable: true
Original file line number Diff line number Diff line change
@@ -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