Skip to content

ExtractionReport: an empty required collection is silently valid — no cardinality vocabulary #201

Description

@dmealing

Summary

The lenient extract route classifies every field it could not populate (LOST_REQUIRED / LOST_OPTIONAL / MALFORMED / DEFAULTED), which is what makes losses audible. But that classification is presence-only. There is no vocabulary for a cardinality invariant, so a payload whose collection came back empty extracts "successfully" with a completely clean reporthasLostRequired() is false.

For the lenient reader's main use case (extracting structured output from a language model) this is the dangerous case, not an edge case: a model that emits a well-formed wrapper full of garbage yields an empty collection, and every downstream consumer sees a valid-but-empty object.

Why @required on the items doesn't cover it

Extract.extract classifies an absent field at its own path and continues — it does not recurse into nested children — and per-item @required is only evaluated for items that are present. So for:

<container>
  <items>
    <item id="A"/>
    <item id="B"/>
  </items>
</container>
  • Marking item.id @required detects "an item that exists but lost its id".
  • It cannot detect "zero items" — with no items present, no per-item check ever runs.
  • Marking the items wrapper @required detects "no <items> element at all", but not <items></items>.

There is no combination of today's markers that makes an empty collection audible.

Ask

A cardinality vocabulary on array fields — e.g. @minItems: 1 — classified as a loss (LOST_REQUIRED, or a new EMPTY_REQUIRED state) so hasLostRequired() fires.

Alternatively, define @required on an isArray field to mean non-empty rather than "the element is present". That is the more intuitive reading, but it is a behavior change for anyone relying on the current semantics.

Workaround today

Assert cardinality in code after every extract — which is precisely the hand-rolled loss check the ExtractionReport otherwise makes unnecessary. It also has to be repeated at each call site, and its absence is silent.

Related: #199

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