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

Author: renechoiCreated Jul 29, 2026Updated Sep 13, 2026

Bug Report Checklist

  • Have you provided a full/minimal spec to reproduce the issue?
  • Have you validated the input using an OpenAPI validator?
  • Have you tested with the latest master to confirm the issue still exists?
  • Have you searched for related issues/PRs?
  • What's the actual output vs expected output?
  • [Optional] Sponsorship to speed up the bug fix or feature request
Description

A schema of a bare type: "null" is resolved to a model named Null / ModelNull that is never generated. This regressed at 7.17.0 and affects every generator, because the cause is in OpenAPINormalizer, not in a language template.

Updated after probing. The first revision of this report described this as a property-level defect and suggested that handling it in normalizeProperties would be enough. That was too narrow. Probing 18 null-bearing shapes across 7.16.0–7.24.0 shows three container sites are affected, and one neighbouring case regressed and was fixed again in a way that rules out the obvious revert. Everything below is the widened version.

Affected sites, all in one model:

typescript
// generated by 7.24.0 — none of these three names exist; models/ holds only child.ts and parent-bare-null.ts
'dyn'?: Null;                 // property:      type: "null"
'dynList'?: Array<Null>;      // array items:   items: {type: "null"}
'dynRef'?: Null;              // $ref to a component schema that is itself type: "null"
'child'?: Child;              // control: correct

7.16.0 produced any | null, Array<any | null> and any | null for the same three. So the regression costs the | null as well as substituting a fictional model.

The failure mode differs by generator, and the quieter one is worse:

7.16.0 7.17.0 → 7.24.0
typescript-axios any | null, no import Null + import type { Null } from './null', and null.ts is never written
java (--library webclient) JsonNullable<Object> / List<Object> ModelNull / List<ModelNull>, and ModelNull.java is never written

Java fails to compile, which is at least visible. TypeScript does not, because the generated // May contain unused imports in some cases / // @ts-ignore pair sits directly above the dangling import and suppresses it. The properties then silently degrade to any while reading as real named types:

typescript
const a: ParentBareNull = { dyn: 12345 };
const b: ParentBareNull = { dyn: 'a string instead' };
const c: ParentBareNull = { dynList: [1, 'two', { three: true }], dynRef: { anything: 'goes' } };
const d = (v: ParentBareNull) => v.dynList![0].whateverMethod().deeply.nested;

tsc --strict exits 0 on all of them.

The shape is not hand-written exotica. @rony-remedio, who found this, reports that Swashbuckle emits a bare type: "null" for an untyped / dynamic member, and measured it on a production 3.1.1 spec (490 component schemas, 486 generated models): it occurs on 4 properties there, all silently degraded to any, confirming the 7.16.0 → 7.24.0 swap four-for-four. Those production figures are theirs; everything else here is from the minimal spec below.

Where this sits relative to what is already fixed

Measured on an 18-shape probe spec with typescript-axios, on 7.16.0, 7.17.0, 7.18.0, 7.19.0, 7.20.0, 7.21.0, 7.22.0, 7.23.0 and 7.24.0:

container branch child shape 7.16.0 7.17.0–7.23.0 7.24.0
map value type: "null" any | null Null any | null — fixed by #23967
map value $ref → bare-null alias any Null any | null — fixed by #23967
parent's own type array $reftype: [null, object] alias Null Null NullObjectAlias | null — fixed by #24140
property type: "null" / ["null"] / with description any | null Null Null
property $ref → bare-null alias any | null Null Null
array items type: "null" Array<any | null> Array<Null> Array<Null>
property type: "null" + enum: [null] no import unused Null import unused Null import

Two things follow.

This is not a "revert the 7.17.0 early return" situation. Row 3 was broken before 7.17.0 and stayed broken until 7.24.0; dropping the early return would put it back. (Attribution of rows 1–3 is by release boundary plus diff inspection; I did not build the intermediate commits.)

The last row is a separate, smaller defect: type: "null" with enum: [null] emits the Null import while typing the property from the inline-enum path, so the import is emitted by something other than what picks the property type.

openapi-generator version

Regression. Bisected on typescript-axios: correct on 7.12.0–7.16.0, broken from 7.17.0 through 7.24.0. A 7.25.0-SNAPSHOT build of the current tree produces output byte-identical to 7.24.0's for this spec (diff -r clean apart from the version stamp), so master is still affected. java --library webclient bisects to the same 7.17.0 boundary.

OpenAPI declaration file content or url
yaml
openapi: 3.1.0
info:
  title: Repro bare null property
  version: 1.0.0
components:
  schemas:
    Child:
      type: object
      properties:
        name:
          type: string

    # a component schema that is itself a bare null type
    NullAlias:
      type: "null"

    ParentBareNull:
      type: object
      properties:
        # 1. property: Swashbuckle emits this shape for an untyped / `dynamic` member
        dyn:
          type: "null"
        # 2. array items
        dynList:
          type: array
          items:
            type: "null"
        # 3. property that $refs a null-typed component schema
        dynRef:
          $ref: "#/components/schemas/NullAlias"
        # control: an ordinary $ref property, which stays correct
        child:
          $ref: "#/components/schemas/Child"
Generation Details
bash
openapi-generator-cli generate -i spec.yaml -g typescript-axios -o out \
  --additional-properties=withSeparateModelsAndApi=true,modelPackage=models,apiPackage=api

Output is byte-identical with and without --openapi-normalizer NORMALIZE_31SPEC=true (diff -r clean), so the rule is not involved either way.

Steps to reproduce
  1. Generate with 7.16.0 → models/parent-bare-null.ts declares 'dyn'?: any | null;, 'dynList'?: Array<any | null>;, 'dynRef'?: any | null; and imports only Child.
  2. Generate the same spec with 7.17.0 or later → Null, Array<Null>, Null plus an import of ./null, while models/ contains only child.ts, index.ts and parent-bare-null.ts.
  3. Run tsc --strict on the output → exits 0. Assign a number, a string or an object to any of the three → still exits 0.
  4. Repeat with -g java --library webclientprivate ModelNull dyn, private List<ModelNull> dynList, private ModelNull dynRef, with no ModelNull.java; model/ holds only Child.java and ParentBareNull.java.
Suggested cause

OpenAPINormalizer#normalizeSchema gained this guard in 7.17.0 (absent at v7.16.0, present at v7.17.0); it is currently at OpenAPINormalizer.java:940:

java
if (ModelUtils.isNullTypeSchema(openAPI, schema)) {
    return schema;
}

ModelUtils.isNullTypeSchema dereferences $refs and returns true for a single-entry type array of "null" (and for a single-entry enum of null), so such a schema is returned untouched no matter which normalizer rules are enabled, and DefaultCodegen#getPrimitiveType then resolves it to the literal type name "null", which becomes a model reference.

Because the early return is inside normalizeSchema itself, a null-type schema can only be rewritten by its parent — and the parent branches do not treat their children alike:

  • map (else if (schema.getAdditionalProperties() instanceof Schema)) checks ModelUtils.isNullTypeSchema(openAPI, additionalProperties) before recursing. That interception is #23967, and it is why rows 1–2 above are fixed.
  • properties (normalizeProperties) calls normalizeSchema(property, new HashSet<>()) on each child with no such check.
  • array calls normalizeSchema(result.getItems(), visitedSchemas) with no such check.

normalizeNullTypeArray31, present in both the properties and map branches, handles the parent's own type array; that is a different axis (it is what #24140 added and what fixed row 3), which is why the parent-level fix left rows 4–6 broken.

So the missing piece is a child-level interception in the properties and array branches, mirroring what the map branch already does.

Related issues/PRs
  • #23945 / #23967 — the map-value form of this, fixed by intercepting in the map branch.
  • #24140 — the type: [object, "null"] parent form, fixed by lifting null out of the parent's type array.
  • #23908 — the Java-side report of this property form.
  • #23961 — a proposed fix in AbstractJavaCodegen; that would leave the other generators broken, since the cause is in the normalizer.
  • #24517 / #24519 — a separate type: [null, object] + additionalProperties: $ref bug. Unrelated code path: whole-tree diff -r between 7.24.0 output and that branch is empty apart from .openapi-generator/VERSION, on all 18 probe shapes.

Found by @rony-remedio while reviewing #24519; filed separately so it doesn't widen that PR's scope.

Source: OpenAPITools/openapi-generator