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 @@ -2363,6 +2363,14 @@ public static boolean isNullTypeSchema(OpenAPI openAPI, Schema schema) {
return false;
}

// OpenAPI 3.0.x: empty nullable object is a null-type schema
if (!(schema instanceof JsonSchema) // 3.0.x only
&& "object".equals(schema.getType())
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 24, 2026

Choose a reason for hiding this comment

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

P2: isNullTypeSchema over-broadly treats nullable object schemas as null sentinels, which can remove valid object branches during oneOf/anyOf simplification.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java, line 2368:

<comment>`isNullTypeSchema` over-broadly treats nullable object schemas as null sentinels, which can remove valid object branches during oneOf/anyOf simplification.</comment>

<file context>
@@ -2363,6 +2363,14 @@ public static boolean isNullTypeSchema(OpenAPI openAPI, Schema schema) {
 
+        // OpenAPI 3.0.x: empty nullable object is a null-type schema
+        if (!(schema instanceof JsonSchema) // 3.0.x only
+                && "object".equals(schema.getType())
+                && Boolean.TRUE.equals(schema.getNullable())
+                && schema.get$ref() == null) {
</file context>
Fix with Cubic

&& Boolean.TRUE.equals(schema.getNullable())
&& schema.get$ref() == 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 @@ -4325,4 +4325,18 @@ public void testJspecify(String library, boolean useSpringBoot4, boolean hasJspe
.fileContains("@org.jspecify.annotations.NullMarked");

}

@Test(description = "anyOf with $ref and {type: object, nullable: true} should resolve to typed nullable field, not Object")
public void testAnyOfNullableObjectSentinelResolvesToTypedField() {
Map<String, File> files = generateFromContract(
"src/test/resources/bugs/issue_anyof_nullable_object_sentinel.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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,35 @@ public void isNullTypeSchemaTest() {

schema = openAPI.getComponents().getSchemas().get("JustDescription");
assertFalse(ModelUtils.isNullTypeSchema(openAPI, schema));

// {type: "object", nullable: true} with no properties is a null sentinel (apispec 6.7.1+)
schema = openAPI.getComponents().getSchemas().get("NullableObjectSentinel");
assertTrue(ModelUtils.isNullTypeSchema(openAPI, schema));

// {type: "object", nullable: true} WITH properties is a real object, not a null sentinel
schema = openAPI.getComponents().getSchemas().get("NullableObjectWithProperties");
assertFalse(ModelUtils.isNullTypeSchema(openAPI, schema));
}

@Test
public void isNullTypeSchemaInlineAnyOfSentinelTest() {
OpenAPI openAPI = TestUtils.parseSpec(
"src/test/resources/bugs/issue_anyof_nullable_object_sentinel.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} — the sentinel
Schema sentinel = anyOf.get(1);
assertTrue(ModelUtils.isNullTypeSchema(openAPI, sentinel),
"Should recognize {type: object, nullable: true} as null sentinel");
}

@Test
Expand Down Expand Up @@ -695,6 +724,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 sentinel — it's a real
// nullable object. Nullability in 3.1 is expressed via type: ["object", "null"].
schema = openAPI.getComponents().getSchemas().get("NullableObjectSentinel");
assertFalse(ModelUtils.isNullTypeSchema(openAPI, schema));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,13 @@ components:
- $ref: '#/components/schemas/IntegerRef'
- $ref: '#/components/schemas/StringRef'
JustDescription:
description: A schema with just description
description: A schema with just description
NullableObjectSentinel:
type: object
nullable: true
NullableObjectWithProperties:
type: object
nullable: true
properties:
name:
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
NullableObjectSentinel:
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 nullable object sentinel 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