Skip to content

[core] Normalize bare null properties and array items in OAS 3.1 specs (#24520) - #25087

Open
SubhamAshok wants to merge 7 commits into
OpenAPITools:masterfrom
SubhamAshok:fix/24520-core-bare-null-property
Open

SubhamAshok wants to merge 7 commits into
OpenAPITools:masterfrom
SubhamAshok:fix/24520-core-bare-null-property

Conversation

@SubhamAshok

@SubhamAshok SubhamAshok commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

PR checklist

Description of the change

In OpenAPI 3.1 specifications, object properties and array items with a bare type: "null" or a $ref to a null-typed schema resolve to an ungenerated model (Null in TypeScript, ModelNull in C# and other generators).

While map values (additionalProperties) were previously normalized to an any-type nullable schema in PR #23967, object properties and array items were left untouched.

This change:

  1. Normalizes bare null properties in normalizeProperties to an any-type nullable schema under NORMALIZE_31SPEC (copying metadata) after schema normalization runs.
  2. Normalizes bare null array items in normalizeSchema under NORMALIZE_31SPEC (copying metadata).
  3. Adds unit tests in OpenAPINormalizerTest and end-to-end verification in TypeScriptAxiosClientCodegenTest.

Summary by cubic

Fixes OAS 3.1 object properties and array items with a bare type: "null" or a $ref to a null-typed schema resolving to ungenerated Null/ModelNull models.

Bug Fixes

  • New NORMALIZE_BARE_NULL_SCHEMAS rule (on by default under NORMALIZE_31SPEC) converts bare null schemas to an any-type nullable schema, preserving metadata across $ref chains and single-member allOf wrappers.
  • cpp-boost-beast opts out by default so OAS 3.1 type: "null" still maps to std::nullptr_t; it can opt in via normalizer configuration.
  • Untyped schemas with validation constraints are left untouched; explicit nullable: false is handled safely.
  • Adds unit tests, a TypeScript Axios end-to-end test, and cpp-boost-beast normalizer tests verifying no Null models are emitted, and updates affected OAS 3.1 samples.

Written for commit 8c07249. Summary will update on new commits.

Review in cubic

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 4 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

@Mattias-Sehlstedt

Copy link
Copy Markdown
Contributor

Isn't the issue that Swashbuckle is heavily misusing type: null, and that it should be an empty schema {}? The correct generator behavior should actually be to have an explicit null type that showcase that it is an field that only accepts null (such a model not being generated properly being another issue)?

Tailoring the normalization to odd behavior from Swashbuckle to me seems like a bad design choice (if anything, you would want some rule adjustToSwashbuckle that is explicitly opt-in).

@SubhamAshok

Copy link
Copy Markdown
Contributor Author

Thanks for the reviews @Mattias-Sehlstedt and @cubic-dev-ai!

@Mattias-Sehlstedt While Swashbuckle emits this for dynamic/untyped fields, type: "null" is also standard JSON Schema in OAS 3.1. Because openapi-generator's internal model lacks a primitive Null type across its generators, any type: "null" schema causes generators to emit fictional Null classes that break compilation. PR #23967 took the approach of normalizing map values (additionalProperties: { type: "null" }) into nullable any-type schemas under NORMALIZE_31SPEC. Extending that to properties and array items keeps NORMALIZE_31SPEC consistent across all containers.

I have also audited the code and pushed commit ac80329b03e addressing the review feedback:

  • Replaced isNullTypeSchema with a strict isBareNullSchema check so untyped constraint-only schemas (such as { pattern: "^[a-z]+$" }) are not modified.
  • Handled $ref schemas with sibling metadata before reference normalization wraps them in allOf.
  • Ensured metadata is copied before setting nullable: true so explicit nullable: false cannot override it.
  • Added assertions in OpenAPINormalizerTest ensuring `` is cleared, and strengthened TypeScriptAxiosClientCodegenTest with exact type checks.
  • Unified the conversion logic across properties, array items, and map values into one helper.

All unit tests and Checkstyle pass cleanly.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 4 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

@SubhamAshok

Copy link
Copy Markdown
Contributor Author

Thanks @cubic-dev-ai, great catch. Pushed commit 0c63bc0b90b to resolve this:

  • Introduced collectResolutionChain to record every schema layer in the resolution path from root through any intermediate $ref and single-member allOf wrappers down to the leaf schema.
  • Added createAnyTypeNullableSchema which iterates the chain from leaf to root, copying metadata and merging extensions at each layer so intermediate wrapper metadata is preserved while outer layers override inner ones.
  • Added tests in OpenAPINormalizerTest and TypeScriptAxiosClientCodegenTest verifying that a referenced allOf wrapper retains its description and custom extensions, and that property-level sibling descriptions override wrapper descriptions as expected.

@cubic-dev-ai

cubic-dev-ai Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Thanks @cubic-dev-ai, great catch. Pushed commit 0c63bc0b90b to resolve this:

  • Introduced collectResolutionChain to record every schema layer in the resolution path from root through any intermediate $ref and single-member allOf wrappers down to the leaf schema.
  • Added createAnyTypeNullableSchema which iterates the chain from leaf to root, copying metadata and merging extensions at each layer so intermediate wrapper metadata is preserved while outer layers override inner ones.
  • Added tests in OpenAPINormalizerTest and TypeScriptAxiosClientCodegenTest verifying that a referenced allOf wrapper retains its description and custom extensions, and that property-level sibling descriptions override wrapper descriptions as expected.

@SubhamAshok Looks right — that addresses the concern I raised. The key fix is that createAnyTypeNullableSchema walks the chain leaf-to-root, so scalar metadata (description, etc.) from outer layers wins while extensions accumulate from every layer. That's exactly what your tests assert: dynRefToAllOfWrapperWithSiblingDesc keeps the sibling description but still carries x-wrapper-ext from the wrapper.

A few things I checked while reading it:

  • The cycle guard (seen) and depth cap make collectResolutionChain safe against self-referential $ref/allOf loops.
  • isBareNullSchema only treats single types: ["null"] as bare null, so ["string","null"] union types are untouched — good.
  • The second isBareNullSchema check after normalizeSchema covers schemas that only become resolvable after normalization (e.g. reference normalization wrapping siblings in allOf), which is a subtle case the first check would miss.
  • The map value else branch dropping result.setAdditionalProperties(normalized) isn't a regression: that assignment was already gated on NORMALIZE_31SPEC, and the 31-spec path now goes through normalizeBareNullSchemaIfNeeded, which still sets the normalized value back.

Unifying properties, array items, and map values through one helper keeps NORMALIZE_31SPEC consistent across all containers, matching the map-value precedent from #23967. Nothing blocking from my side.

Tip: get faster answers by chatting with cubic’s review copilot in the review UI

In OpenAPI 3.1 specifications, object properties and array items with
a bare type: "null" or a $ref to a null-typed schema resolve to a
fictional Null or ModelNull model in generators lacking a null type mapping.
While map values were previously normalized to an any-type nullable schema,
properties and array items were left untouched.

Normalize bare null object properties and array item schemas to an
any-type nullable schema under NORMALIZE_31SPEC, preserving schema
metadata.

Fixes OpenAPITools#24520
@SubhamAshok
SubhamAshok force-pushed the fix/24520-core-bare-null-property branch from 0c63bc0 to 7dc39e5 Compare October 2, 2026 18:23
…for cpp-boost-beast

- Add NORMALIZE_BARE_NULL_SCHEMAS normalizer rule (defaulted to true) under NORMALIZE_31SPEC.

- Allow generators with native null support to opt out via normalizer configuration or custom normalizer overrides.

- Override normalizeBareNullSchemaIfNeeded in CppBoostBeastOpenAPINormalizer and configure cpp-boost-beast client and server to preserve OAS 3.1 type: "null" as std::nullptr_t.

- Add OpenAPINormalizerTest coverage for NORMALIZE_BARE_NULL_SCHEMAS=false.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 5 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

…izer when rule enabled

- Allow CppBoostBeastOpenAPINormalizer to delegate to super.normalizeBareNullSchemaIfNeeded when NORMALIZE_BARE_NULL_SCHEMAS is enabled

- Default NORMALIZE_BARE_NULL_SCHEMAS to false in CppBoostBeastModelCodegen and CppBoostBeastOpenAPINormalizer

- Add tests in CompositionNormalizationTest verifying default preservation and opt-in normalization

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 3 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

…onTest

- Move ModelUtils import below meta package imports

- Wrap parseSpec lines to stay within 100 character line limit

This branch has not been deployed

No deployments
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.

[BUG][core] OAS 3.1 property with a bare type: "null" resolves to a never-generated Null / ModelNull model (regression in 7.17.0)

2 participants