fix: recognize {type: object, nullable: true} as null-type schema in OpenAPI 3.0.x#23621
Conversation
…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>
There was a problem hiding this comment.
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.
|
As far as I know the established way of expressing nullable object refs in OAS 3.0 is myField:
nullable: true
allOf:
- $ref: '#/components/schemas/MySchema'I would argue that the suggested schema is not preferable since it states " |
isn't it just any type which is also nullable? ref: https://swagger.io/docs/specification/v3_0/data-models/data-types/#any-type |
Per OAS 3.0 §4.4, "any type" is an untyped schema ( You're right that it's not a precise null representation. But OAS 3.0 has no
The
The 3.0.3-compliant alternative is an The problem is expressing the null branch — OAS 3.0 has no This PR recognizes that pattern pragmatically: a nullable object schema with no properties, no additionalProperties, and no |
|
@Mattias-Sehlstedt |
|
If the intent is to follow the standard to the strongest degree possible, shouldn't the schema be: myNullableField:
allOf:
- $ref: '#/components/schemas/MySchema'
myField:
not:
enum: [null]
allOf:
- $ref: '#/components/schemas/MySchema'
MySchema:
type: object
nullable: true
properties:
property:
type: stringAs per the suggestion from the proposal. What would be the reason for apispec not expressing it as: shippingAddress:
anyOf:
- $ref: '#/components/schemas/Address'
- type: object
nullable: true <- or even emit this entirely?
enum: [null]to strictly capture that the second object is null? To my understand that schema is still valid and expresses a strict validation rule for either |
|
Both suggestions ( This PR sits on the consumer side of Postel's Law: "be conservative in what you send, be liberal in what you accept." We can't dictate to every team and generator what form their output takes — only whether it conforms to the spec. |
|
Note/clarification: I am only a contributor, all opinions are my own and does not affect the review/PR in any way. I disagree with Postel's Law applying completely for this scenario, as per "..., but programs that receive messages should accept non-conformant input as long as the meaning is clear". There are a lot of possible way of trying to express nullability of a ref in OAS 3.0 as we have seen, and I would argue that the one apispec has can be improved. So why would one introduce logic to handle less clear cases in the openapi-generator, rather than ensuring that apispec uses a better representation? If the generator does not take the opportunity to forward knowledge of how things are best structured in OAS, then I think one misses an opportunity to work towards a consensus and shared understanding. When I work in this repo I usually find that there are many occasions where the same thing is done in different ways, when it would have been very preferable to ensure that the internal understanding was the same everywhere (e.g., when different language Codegens does oneOf interpretation slightly differently everywhere). And if someone is stuck with a representation that does not fit directly into how the repo interprets specifications, then it offers plenty of opportunity to customize the behavior so that the client can create a bridge from their specification to an "openapi-generator-specification" (e.g., with a custom normalizer). |
Summary
isNullTypeSchema()does not recognize{type: "object", nullable: true}(with no properties) as a null-type schema. This causes theSIMPLIFY_ONEOF_ANYOFnormalizer to fail to simplifyanyOfschemas where this pattern is used as the nullable branch, producingObjector synthetic wrapper classes instead of typed nullable fields.This is a valid OpenAPI 3.0.x idiom for expressing nullability alongside a
$refinanyOf/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:typefield at alltype: "null"(OpenAPI 3.1)nullenumIt 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 existingSIMPLIFY_ONEOF_ANYOFlogic insimplifyOneOfAnyOfWithOnlyOneNonNullSubSchemacorrectly identifies theanyOfas containing one real schema and one null sentinel, and simplifies it to a typed nullable field.Before (input spec)
Before (generated Java)
After (generated Java)
Why this pattern is spec-correct
The OpenAPI 3.0.3 spec states that
nullable: trueonly takes effect whentypeis "explicitly defined within the same Schema Object." Since$refresolves to a different Schema Object,nullable: truealongsideallOf: [{$ref: ...}]is technically ineffective. TheanyOfapproach with a nullable empty object is the correct OpenAPI 3.0.x idiom — see analysis by Dan Ott (cited by apispec maintainers).Test coverage
isNullTypeSchemaTesttrue; sentinel with properties →falseisNullTypeSchemaTestWith31Specfalse(not a null sentinel in 3.1)isNullTypeSchemaInlineAnyOfSentinelTesttestAnyOfNullableObjectSentinelResolvesToTypedFieldAddressfield, noOrderShippingAddresswrapperImpact
Affects any OpenAPI 3.0.x spec using
anyOf/oneOfwith{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 onSIMPLIFY_ONEOF_ANYOFshould benefit.Related issues
isNullTypeSchemaincorrectly equates missingtypewith nullableanyOfsimplificationSIMPLIFY_ONEOF_ANYOFskips valid single-subschema casesallOf+nullabletoanyOfwith nullable sentinelTest plan
ModelUtilsTest#isNullTypeSchemaTestpassesModelUtilsTest#isNullTypeSchemaTestWith31Specpasses (no 3.1 regression)ModelUtilsTest#isNullTypeSchemaInlineAnyOfSentinelTestpassesJavaClientCodegenTest#testAnyOfNullableObjectSentinelResolvesToTypedFieldpasses🤖 Generated with Claude Code
Summary by cubic
Fixes null-type detection for OpenAPI 3.0.x when
anyOf/oneOfuses{type: object, nullable: true}as the null branch. Generators now produce typed nullable fields instead ofObjector synthetic wrappers.ModelUtils.isNullTypeSchema()to treat a 3.0.x nullable empty object with no properties, noadditionalProperties, and no$refas a null sentinel; objects with properties or maps are not treated as null. This does not apply to 3.1.bugs/issue_anyof_bare_nullable_object.yamlvalidating$ref+ sentinel collapses to a single nullable typed field with no wrapper.Written for commit a54ffbc. Summary will update on new commits. Review in cubic