A schema that co-declares oneOf/anyOf with structural keywords lowers its structural body and keeps the union verbatim (lowerBesideUnmodeledUnion, compilers/openapi/internal/schema/schema.go). When that body reduces to a shared primitive, the position hoists an alias node so the preserved union has somewhere to attach — and that alias is interned with nil constraints, so every value constraint written at the position is dropped, silently.
Reproduction
openapi: 3.1.0
info: {title: T, version: 1.0.0}
paths: {}
components:
schemas:
A:
type: string
minLength: 3
oneOf: [{minLength: 1}, {minLength: 2}]
$ morphic compile a.yaml -skip-validate | jq -c '.types["t/openapi/components/schemas/A"] | {kind, constraints, u: (.unmodeled | keys)}'
{"kind":"scalar","constraints":null,"u":["openapi:oneOf"]}
minLength: 3 reaches no field of the document and no diagnostic names it. The only diagnostic emitted is the one about the union. A numeric pair behaves the same way: {type: string, minimum: 10, exclusiveMinimum: 0, oneOf: [...]} produces constraints: null too.
Why it happens
Every other alias hoist reads the position's constraints first — lowerComponentSchema, hoistDeclarationHome, hoistSubSchema and preserveUnhomedKeywords all call schemaConstraints and pass the result to internAlias. This one passes nil. Ownership is what makes it terminal: hoistDeclarationHome resolves to whatever node the pointer already owns and returns early, so the fallback that would otherwise carry the constraints never runs.
A position that also writes an unhomed keyword happens to be rescued, because preserveUnhomedKeywords interns its own alias through the constraint-reading path first. Remove the unhomed keyword and the loss appears — which is why it has gone unnoticed.
Expected
The alias hoisted beside an unmodeled union carries the position's value constraints, exactly as every other alias hoist does. This is the same rule hoistByteScalar states for itself: owning a node is what stops the declaration-home fallback carrying them, so a node that hoists for its own reasons must carry them itself.
A schema that co-declares
oneOf/anyOfwith structural keywords lowers its structural body and keeps the union verbatim (lowerBesideUnmodeledUnion,compilers/openapi/internal/schema/schema.go). When that body reduces to a shared primitive, the position hoists an alias node so the preserved union has somewhere to attach — and that alias is interned withnilconstraints, so every value constraint written at the position is dropped, silently.Reproduction
minLength: 3reaches no field of the document and no diagnostic names it. The only diagnostic emitted is the one about the union. A numeric pair behaves the same way:{type: string, minimum: 10, exclusiveMinimum: 0, oneOf: [...]}producesconstraints: nulltoo.Why it happens
Every other alias hoist reads the position's constraints first —
lowerComponentSchema,hoistDeclarationHome,hoistSubSchemaandpreserveUnhomedKeywordsall callschemaConstraintsand pass the result tointernAlias. This one passesnil. Ownership is what makes it terminal:hoistDeclarationHomeresolves to whatever node the pointer already owns and returns early, so the fallback that would otherwise carry the constraints never runs.A position that also writes an unhomed keyword happens to be rescued, because
preserveUnhomedKeywordsinterns its own alias through the constraint-reading path first. Remove the unhomed keyword and the loss appears — which is why it has gone unnoticed.Expected
The alias hoisted beside an unmodeled union carries the position's value constraints, exactly as every other alias hoist does. This is the same rule
hoistByteScalarstates for itself: owning a node is what stops the declaration-home fallback carrying them, so a node that hoists for its own reasons must carry them itself.