Skip to content

openapi: oneOf/anyOf beside a $ref vanishes without a diagnostic #406

Description

@OmarAlJarrah

A schema that writes oneOf/anyOf beside a $ref loses the union outright. It reaches no
field of the document, it is not kept under Unmodeled, and nothing is reported.

Reproduction

openapi: 3.1.0
info: {title: T, version: "1"}
paths: {}
components:
  schemas:
    Base: {type: object, properties: {id: {type: string}}}
    G: {$ref: '#/components/schemas/Base', oneOf: [{type: string}, {type: integer}]}
    H: {$ref: '#/components/schemas/Base', anyOf: [{type: string}, {type: integer}]}
$ morphic compile refunion.yaml -skip-validate | jq -c '.types["t/openapi/components/schemas/G"] | {kind, base, u: .unmodeled}'
{"kind":"scalar","base":{"target":"t/openapi/components/schemas/Base","nullable":false},"u":null}

$ morphic compile refunion.yaml -skip-validate 2>&1 >/dev/null | wc -l
0

anyOf behaves identically. Under 2020-12 a $ref conjoins with its siblings, so the union
narrows the referenced schema; dropping it silently widens what the SDK accepts and leaves no
trace that it ever existed.

Why it happens

The union family has one keeper, preserveUnionSiblings, and it is reached only through
lowerBesideUnmodeledUnion — the structural-body path. A declaration that peels a leading
$ref never gets there. The census that catches the other sibling keywords cannot cover for
it either: censusKeywords lists additionalProperties, const, enum, format, items,
maxItems, minItems, patternProperties, prefixItems, properties, required, type
and uniqueItems, and oneOf/anyOf are in neither that list nor keywordHome's switch.

So the union falls between the two mechanisms: the path that would keep it is not taken, and
the path that is taken does not look for it.

#283 covered format/enum/const/required/additionalProperties beside a $ref and was
fixed by #348; the union family was not in its list and is still open.

Expected

oneOf/anyOf written beside a $ref is kept verbatim under Unmodeled on the node the
position owns, with the same degraded-construct report the structural-body path already emits,
rather than disappearing.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions