Skip to content

openapi: a bare enum with a null member does not normalize to a nullable Enum #265

Description

@OmarAlJarrah

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions