Skip to content

fix: recognize {type: object, nullable: true} as null-type schema in OpenAPI 3.0.x#23621

Open
ericdriggs wants to merge 1 commit intoOpenAPITools:masterfrom
ericdriggs:fix/3.0-nullable-object-null-type-detection
Open

fix: recognize {type: object, nullable: true} as null-type schema in OpenAPI 3.0.x#23621
ericdriggs wants to merge 1 commit intoOpenAPITools:masterfrom
ericdriggs:fix/3.0-nullable-object-null-type-detection

Conversation

@ericdriggs
Copy link
Copy Markdown

@ericdriggs ericdriggs commented Apr 24, 2026

Summary

isNullTypeSchema() does not recognize {type: "object", nullable: true} (with no properties) as a null-type schema. This causes the SIMPLIFY_ONEOF_ANYOF normalizer to fail to simplify anyOf schemas where this pattern is used as the nullable branch, producing Object or synthetic wrapper classes instead of typed nullable fields.

This is a valid OpenAPI 3.0.x idiom for expressing nullability alongside a $ref in anyOf/oneOf. It is produced by apispec >= 6.7.1 (PR #953) and potentially other spec generators.

Root cause

isNullTypeSchema() checks for null-type schemas by looking for:

  1. No type field at all
  2. type: "null" (OpenAPI 3.1)
  3. A single-value null enum

It did not check for {type: "object", nullable: true} with no properties and no $ref — an empty nullable object whose only purpose is to express "this value can be null" in OpenAPI 3.0.x.

Fix

Add a check to isNullTypeSchema() for empty nullable objects, scoped to OpenAPI 3.0.x only via !(schema instanceof JsonSchema). This follows the same 3.0/3.1 discrimination pattern used elsewhere in the same method.

Once isNullTypeSchema() recognizes this pattern, the existing SIMPLIFY_ONEOF_ANYOF logic in simplifyOneOfAnyOfWithOnlyOneNonNullSubSchema correctly identifies the anyOf as containing one real schema and one null sentinel, and simplifies it to a typed nullable field.

Before (input spec)

shippingAddress:
  anyOf:
    - $ref: '#/components/schemas/Address'
    - type: object
      nullable: true

Before (generated Java)

private OrderShippingAddress shippingAddress; // synthetic wrapper class

After (generated Java)

private JsonNullable<Address> shippingAddress; // typed nullable field

Why this pattern is spec-correct

The OpenAPI 3.0.3 spec states that nullable: true only takes effect when type is "explicitly defined within the same Schema Object." Since $ref resolves to a different Schema Object, nullable: true alongside allOf: [{$ref: ...}] is technically ineffective. The anyOf approach with a nullable empty object is the correct OpenAPI 3.0.x idiom — see analysis by Dan Ott (cited by apispec maintainers).

Test coverage

Test What it verifies
isNullTypeSchemaTest 3.0 sentinel → true; sentinel with properties → false
isNullTypeSchemaTestWith31Spec 3.1 sentinel → false (not a null sentinel in 3.1)
isNullTypeSchemaInlineAnyOfSentinelTest Inline anyOf sub-schema sentinel recognized
testAnyOfNullableObjectSentinelResolvesToTypedField End-to-end Java codegen: Address field, no OrderShippingAddress wrapper

Impact

Affects any OpenAPI 3.0.x spec using anyOf/oneOf with {type: "object", nullable: true} as the nullable branch, including all specs generated by apispec >= 6.7.1 (current latest: 6.10.0). All language targets that rely on SIMPLIFY_ONEOF_ANYOF should benefit.

Related issues

Test plan

  • ModelUtilsTest#isNullTypeSchemaTest passes
  • ModelUtilsTest#isNullTypeSchemaTestWith31Spec passes (no 3.1 regression)
  • ModelUtilsTest#isNullTypeSchemaInlineAnyOfSentinelTest passes
  • JavaClientCodegenTest#testAnyOfNullableObjectSentinelResolvesToTypedField passes

🤖 Generated with Claude Code


Summary by cubic

Fixes null-type detection for OpenAPI 3.0.x when anyOf/oneOf uses {type: object, nullable: true} as the null branch, common in specs from apispec 6.7.1+. Generators now produce typed nullable fields instead of Object or wrapper classes.

  • Bug Fixes
    • Update ModelUtils.isNullTypeSchema() to treat an empty nullable object as a null sentinel in 3.0.x only (!(schema instanceof JsonSchema)).
    • Enables SIMPLIFY_ONEOF_ANYOF to collapse $ref + null sentinel into a single nullable type; no synthetic wrapper classes.
    • Adds tests for 3.0.x and 3.1 to validate detection and end-to-end Java codegen.

Written for commit f651452. Summary will update on new commits.

…OpenAPI 3.0.x

The SIMPLIFY_ONEOF_ANYOF normalizer failed to simplify anyOf schemas
where the nullable branch uses {type: "object", nullable: true} instead
of an untyped schema or {type: "null"}. This caused Java (and likely
other) code generators to produce Object or synthetic wrapper classes
instead of the intended typed nullable field.

This pattern is a valid OpenAPI 3.0.x idiom for expressing nullability
alongside a $ref in anyOf/oneOf. It is produced by apispec >= 6.7.1
(the most widely used OpenAPI spec generator for Python/Flask/Marshmallow)
and potentially other spec generators.

Root cause: isNullTypeSchema() did not recognize an empty nullable object
({type: "object", nullable: true} with no properties and no $ref) as a
null-type schema. The fix adds a check for this pattern, scoped to 3.0.x
only via !(schema instanceof JsonSchema), since OpenAPI 3.1 expresses
nullability differently via type arrays.

Test coverage:
- isNullTypeSchemaTest: 3.0 sentinel (true), sentinel with properties (false)
- isNullTypeSchemaTestWith31Spec: 3.1 sentinel correctly returns false
- isNullTypeSchemaInlineAnyOfSentinelTest: inline anyOf sub-schema recognized
- testAnyOfNullableObjectSentinelResolvesToTypedField: end-to-end Java
  codegen produces Address field, no synthetic OrderShippingAddress wrapper

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 6 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java">

<violation number="1" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java:2368">
P2: `isNullTypeSchema` over-broadly treats nullable object schemas as null sentinels, which can remove valid object branches during oneOf/anyOf simplification.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.


// 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants