OpenAPITools / OpenAPITools/openapi-generator
[BUG][core] OAS 3.1 property with a bare type: "null" resolves to a never-generated Null / ModelNull model (regression in 7.17.0)
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 26.8k
- Forks
- 7.7k
- PR merge metrics
- PR metrics pending
Description
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
normalizePropertieswould 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:
// 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:
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 | $ref → type: [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
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
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
- Generate with 7.16.0 →
models/parent-bare-null.tsdeclares'dyn'?: any | null;,'dynList'?: Array<any | null>;,'dynRef'?: any | null;and imports onlyChild. - Generate the same spec with 7.17.0 or later →
Null,Array<Null>,Nullplus an import of./null, whilemodels/contains onlychild.ts,index.tsandparent-bare-null.ts. - Run
tsc --stricton the output → exits 0. Assign a number, a string or an object to any of the three → still exits 0. - Repeat with
-g java --library webclient→private ModelNull dyn,private List<ModelNull> dynList,private ModelNull dynRef, with noModelNull.java;model/holds onlyChild.javaandParentBareNull.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:
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)) checksModelUtils.isNullTypeSchema(openAPI, additionalProperties)before recursing. That interception is #23967, and it is why rows 1–2 above are fixed. - properties (
normalizeProperties) callsnormalizeSchema(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 liftingnullout 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: $refbug. Unrelated code path: whole-treediff -rbetween 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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in OpenAPINormalizer.java around normalizeSchema and trace the normalizeProperties and array-item branches, comparing them with the existing map-value interception. Reproduce the supplied spec with typescript-axios and java webclient, then verify that bare null properties, array items, and $refs retain null-compatible output without fictional model references or missing imports.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java, typescript
- Domain
- api, tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100