Summary
{enum: [red, green, null]} — a null member with no type keyword beside it — still lowers to a
Union of three Literals with the openapi/degraded-construct info diagnostic, rather than to
Enum{red, green} with Nullable on the references to it.
In the 2020-12 dialect a bare enum constrains the instance to exactly the listed values, so this
schema does admit null and the members are otherwise homogeneous strings. ir-design.md §3.3 would
have that null normalize onto TypeRef.Nullable, which is what the type-array spelling
({type: [string, "null"], enum: [red, green, null]}) now does.
Why it was left out of #44
Nothing is lost today — the null survives as a ValueNull Literal variant and the degradation is
diagnosed — so this is under-normalization, not data loss, and closing it is a wider change than the
one #44 needed.
schemaAdmitsNull (compilers/openapi/internal/schema/schema.go) is the single predicate every site
computing a Nullable bit goes through: the declaration position, a union, and refNullable at every
$ref use site. It reads nullable: true and the type array, and does not read enum members. So
stripping the null member here without touching it would set the bit nowhere — the null would
simply vanish, which is the failure #28/#101 fixed for the other spellings. Making it work means
teaching schemaAdmitsNull that a null enum member admits null.
What that change has to decide
{type: string, enum: [red, green, null]} — keywords conjoin, so type: string excludes null and
the member is dead. This must not become nullable; treating it as nullable widens the type the
document declared. (It is also arguably wrong today, since the union-of-literals fallback does emit
a null literal variant for it.)
{enum: [red, green, null], allOf: [{type: string}]} and other shapes where the type constraint
arrives from a composed branch rather than a sibling keyword.
- Whether a heterogeneous set (
{enum: [1, "a", null]}) should end up with both a null Literal
variant and a nullable ref, which is what a naive widening produces.
Repro
openapi: 3.1.0
info: {title: t, version: '1'}
paths: {}
components:
schemas:
Color:
enum: [red, green, null]
Today: Color is a Union of Literals red, green, null, plus
openapi/degraded-construct: heterogeneous or non-scalar enum lowered as a union of literals.
TestEnum_NullMemberKeepsUnionFallback/no_type_keyword_to_admit_null pins that behaviour, so it is
the test to change when this is settled.
Summary
{enum: [red, green, null]}— anullmember with notypekeyword beside it — still lowers to aUnion of three Literals with the
openapi/degraded-constructinfo diagnostic, rather than toEnum{red, green}withNullableon the references to it.In the 2020-12 dialect a bare
enumconstrains the instance to exactly the listed values, so thisschema does admit null and the members are otherwise homogeneous strings.
ir-design.md§3.3 wouldhave that null normalize onto
TypeRef.Nullable, which is what the type-array spelling(
{type: [string, "null"], enum: [red, green, null]}) now does.Why it was left out of #44
Nothing is lost today — the null survives as a
ValueNullLiteral variant and the degradation isdiagnosed — so this is under-normalization, not data loss, and closing it is a wider change than the
one #44 needed.
schemaAdmitsNull(compilers/openapi/internal/schema/schema.go) is the single predicate every sitecomputing a Nullable bit goes through: the declaration position, a union, and
refNullableat every$refuse site. It readsnullable: trueand thetypearray, and does not read enum members. Sostripping the null member here without touching it would set the bit nowhere — the null would
simply vanish, which is the failure #28/#101 fixed for the other spellings. Making it work means
teaching
schemaAdmitsNullthat a null enum member admits null.What that change has to decide
{type: string, enum: [red, green, null]}— keywords conjoin, sotype: stringexcludes null andthe member is dead. This must not become nullable; treating it as nullable widens the type the
document declared. (It is also arguably wrong today, since the union-of-literals fallback does emit
a null literal variant for it.)
{enum: [red, green, null], allOf: [{type: string}]}and other shapes where the type constraintarrives from a composed branch rather than a sibling keyword.
{enum: [1, "a", null]}) should end up with both a null Literalvariant and a nullable ref, which is what a naive widening produces.
Repro
Today:
Coloris a Union of Literalsred,green,null, plusopenapi/degraded-construct: heterogeneous or non-scalar enum lowered as a union of literals.TestEnum_NullMemberKeepsUnionFallback/no_type_keyword_to_admit_nullpins that behaviour, so it isthe test to change when this is settled.