Skip to content

JSON locator takes the first object that parses — a fenced answer after any brace-bearing prose is silently discarded #363

Description

@dmealing

Affected port(s): Python (behaviour likely shared — please check TS/Java/C#)
Package + version: metaobjects 1.0.0 (PyPI)

What happened

The JSON payload locator behind extract_object(...) takes the first object that
parses
and stops. It does not prefer a fenced block, and it does not fall through
when that first object fails the declared shape.

So any object-shaped text appearing before the real answer wins, and a perfectly good
reply sitting in a following ```json fence is never reached. Because the
declared @required fields are then missing, the reply is reported as unusable — the
answer is not mis-parsed, it is silently discarded.

Measured against 1.0.0 with this value-object:

metadata:
  package: probe
  children:
    - object.value:
        name: gate
        children:
          - field.string:  {name: topic}
          - field.boolean: {name: worth_forming, required: true}
          - field.string:  {name: reason, required: true}

and GOOD = {"topic":"real","worth_forming":true,"reason":"r"}:

input topic has_lost_required()
GOOD 'real' False
```json\nGOOD\n``` 'real' False
Thinking...\n```json\nGOOD\n``` 'real' False
{"topic":"draft_idea"}\n```json\nGOOD\n``` 'draft_idea' True
The format is {"topic": "<slug>"}.\n```json\nGOOD\n``` '<slug>' True
Consider {a set} of options.\n```json\nGOOD\n``` None True

The last three are the defect. In each, GOOD is present and complete in the fence.

The trigger is ordinary model behaviour, not adversarial input: a model that reasons
before answering writes a partial object in its reasoning, or echoes the requested
format back, or simply writes prose containing a brace.

What you expected

Two changes, both of which the locator has enough information to make:

  1. Prefer fenced blocks. When the reply contains ``` blocks, look inside
    them before scanning the whole text. Models are explicitly instructed to fence, so
    the fenced block is the answer and anything outside it is commentary.
  2. Fall through on shape mismatch. An object that does not carry any declared field
    should not end the search. Keep scanning; a later, better-shaped object should win.
    (Symmetrically: a fenced block of the wrong shape — a fenced example, a fenced note —
    must not shadow a real answer elsewhere, so fence-preference needs the same
    fall-through.)

With both, all six rows above resolve to topic='real'.

Why this matters beyond the rows

This is the failure mode that makes adopters hand-roll their own readers instead of
using the generated extract. In one Python codebase I audited there were six
independent JSON extractors, each having separately discovered a different variation of
dirty model output. Two measurements from that codebase's production telemetry, on
replies stored verbatim:

  • one prompt: 80 calls, 79 fenced, and a naive reader rejected every one — the
    subsystem produced nothing for ten days and nobody noticed, because its failure was
    indistinguishable from its "nothing to report";
  • another prompt: 22 of 270 replies (8.1%) lost to commentary after the closing
    fence, degrading retrieval on roughly one turn in twelve.

Both are exactly what the generated extract should make unnecessary. It nearly does —
1.0.0 already handles fenced, prose-before, commentary-after, two fenced blocks, and
even a trailing comma at NORMAL tolerance, which is more than several of those
hand-rolled readers managed. The first-object-wins rule is the remaining gap, and while
it stands, an adopter's hand-rolled reader is measurably more capable than the
platform's — which is the wrong way round.

Reproduction

Load the metadata above, then:

from metaobjects.meta.core.object.object_extract import extract_object
from metaobjects.render import Format

GOOD = '{"topic":"real","worth_forming":true,"reason":"r"}'
text = '{"topic":"draft_idea"}\n```json\n' + GOOD + '\n```'

out = extract_object(mo, text, Format.JSON, None)
# actual:   topic='draft_idea', has_lost_required() == True
# expected: topic='real',       has_lost_required() == False

Environment

Linux, CPython 3.11.14, metaobjects 1.0.0 from PyPI, installed with pip.

Related

Filed alongside a separate FR for the extension-surface gap this exposed — the override
surface (ExtractOptions.on_field / normalizers / tolerance) is per-field-value
only, so an adopter cannot supply a payload-location strategy without forking.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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