project-039 · LLM and GenAI · aviation
Structured extraction from disruption notices: JSON-schema validation, safe repair and injected-instruction detection
An airline operations team wants free-text disruption notices turned into a JSON record (flight, date, airports, disruption type, delay minutes, cause category, whether rebooking was offered) that downstream automation can trust. Before connecting any model, the team needs a validation layer: how many outputs pass the schema, how many can be repaired without inventing values, how many are schema-valid but wrong, and how reliably are outputs that followed an instruction hidden in the notice caught?
At a glance
Data
Synthetic airline disruption notices with gold JSON and labelled recorded-output fixtures (academy-authored) (synthetic) · Reference run: 100 synthetic notices and 182 labelled output fixtures (valid 25, syntax 26, schema 47, semantic 51, injection-followed 18, injection-ignored 15)
Details on dataBaseline
Parse with the standard JSON parser and validate with the schema; anything that fails goes to human review, with no repair.
Details on baselineValidation
Split notices 50/50 into development and test before writing repair rules; rules and detector thresholds are written on development fixtures only and frozen before the…
Details on validationMetric
Schema-valid rate before and after repair; harmful-repair count; per-field exact match against gold; silent-wrong rate; injection detector precision/recall;
Details on metric
Success criterion
On the labelled fixture set, the validator must catch 100% of schema violations (a deterministic requirement), repairs must never change a value that was present and correct (zero harmful repairs), and the injected-instruction detector must reach recall of at least 0.90 at precision of at least 0.80 on the injection-labelled fixtures (targets, not results). The operational number to report is the "silent wrong" rate: schema-valid outputs with a wrong field value, which validation alone cannot catch.
Context
Day 47 teaches that a schema is a contract and that model output is untrusted input. Provider features that constrain output to a schema reduce syntax failures but do not make values correct. This project builds the layer that sits between any generator and the business system. The reference implementation works on recorded output fixtures - hand-authored, clearly labelled examples of what model outputs look like - not on live generations, so it measures the validator, not a model. With a configured model the same validator runs on live outputs.
Data
- Dataset
- Synthetic airline disruption notices with gold JSON and labelled recorded-output fixtures (academy-authored) (synthetic)
- Licence
- Academy-authored synthetic data; no third-party licence
- Size
- Reference run: 100 synthetic notices and 182 labelled output fixtures (valid 25, syntax 26, schema 47, semantic 51, injection-followed 18, injection-ignored 15)
- Verification
- unverified
Access
Generated and stored with the reference implementation (no network, no login). Notices are synthetic text in the style of the academy's synthetic airline world (routes such as IST-FRA, flight numbers and dates consistent with python/artifacts/airline_sample.sqlite). Each notice has a gold JSON record. Each recorded-output fixture is a hand-authored string labelled with its fault class; fixtures are not outputs of any real model and are labelled "recorded fixture" in every file and report.
Grain
One row per (notice, fixture output); fault labels are per fixture; gold values are per notice field.
| Column | Meaning |
|---|---|
| notice_id / notice_text | Synthetic disruption notice; some contain an embedded instruction (labelled) |
| gold | Correct JSON record for the notice, validated against the schema |
| fixture_output | Recorded output string as a model might return it (labelled fixture, not a live generation) |
| fault_class | valid, syntax (fences, trailing comma, truncation), schema (missing required, wrong type, bad enum, extra field), semantic (valid but wrong value), injection-followed, injection-ignored |
| injected | True when the notice text contains an instruction aimed at the extractor |
| expected_route | accept, accept-after-repair, or human-review - the label the pipeline decision is scored against |
EDA and data-quality rules
Every gold record validates against the JSON schema; the schema itself is versioned and tested with valid and invalid examples.
Fault classes are balanced enough to estimate each rate (at least 15 fixtures per class) and every fixture has exactly one primary fault label.
Semantic checks are explicit rules (origin differs from destination, delay minutes non-negative, date parses and lies within the notice's stated window, flight number pattern) and each is tested.
Injection fixtures are harmless (they ask for a field change, an extra field, or a fake e-mail action to example.invalid) and labelled in the file.
Baseline
Parse with the standard JSON parser and validate with the schema; anything that fails goes to human review, with no repair. This "strict reject" baseline has zero harmful repairs by construction, so every repair strategy must justify the human-review load it removes.
Features
Repair operations, each logged: strip markdown code fences and leading prose; remove trailing commas; normalise enum case and a small synonym map (e.g. "cancelled"/"canceled"); coerce numeric strings to integers when lossless. Never repaired: missing required fields, out-of-range values, values that disagree with the notice. Cross-checks against the source: flight number and airports must appear in the notice text. Injection signals: imperative phrases addressed to the system in the notice, e-mail addresses or URLs in output fields that have none in gold, extra fields, and boolean fields that flip in the direction requested by the embedded instruction.
Candidate methods
Deterministic repair plus semantic rules
Recovers formatting faults cheaply while every rule is auditable and unit-tested.
Rule-based injected-instruction detector on input and output
Detects the pattern 'notice asked for X and output did X'; transparent and runs before any downstream action.
Re-ask the model with the validation error (live mode only)
Often fixes schema faults without rules, but costs a second call, can introduce new errors and cannot run in fixture mode.
Validation design
Split notices 50/50 into development and test before writing repair rules; rules and detector thresholds are written on development fixtures only and frozen before the test fixtures are scored. In live mode, run each notice three times to measure output variability and score all runs.
Leakage checks
The same author writes fixtures and rules, so rules can be tailored to known fixtures. Freeze the test fixtures before writing rules, and add ten test fixtures written by someone else (or after a week) to estimate overfitting.
Bias and representativeness
Notices follow a few templates in English; real notices vary by station and language. Report results by template and add a small set of free-form notices to show the drop.
Robustness checks
Unicode look-alikes in flight numbers, very long notices that truncate output, numbers written as words, and two flights in one notice (the schema expects one - correct behaviour is human review).
Technical metrics
Schema-valid rate before and after repair; harmful-repair count; per-field exact match against gold; silent-wrong rate; injection detector precision/recall; routing accuracy against expected_route.
Business interpretation
Share of notices that flow straight through, share sent to human review (staff time), and the count of silent-wrong records that would reach automation - the number the operations owner signs off.
Error slices
Fault class: syntax, schema, semantic and injection fixtures have different catch mechanisms; report each.
Field: delay_minutes and cause_category are typically harder than flight number and date.
Notice template and length.
Implementation steps
Write the JSON schema (required fields, enums, integer ranges, additionalProperties false) and tests with valid/invalid examples.
Generate notices and gold records with a seeded script; hand-author fixtures per fault class and label them.
Implement parse, validate, repair (logged operations) and semantic/source cross-checks; unit-test each repair as a pure function.
Implement the injection detector and the routing decision (accept / accept-after-repair / human-review).
Score development fixtures, freeze rules, score test fixtures once, and write the report with the confusion matrix of routes.
Optional live mode: run the same pipeline on outputs from a configured model with schema-constrained output switched on and off, logging model id and date.
Reproduction
Reference implementation at python/reference/r09_structured_output_validation (status: implemented; recorded results in its RESULTS.md). Command: cd python && uv run python -m reference.r09_structured_output_validation.run. CPU-only, no network, no model, no API keys; fixtures are seeded or hand-authored files in the implementation folder; the validator is a small standard-library JSON-schema subset, so no extra dependency is needed (your learner version may use the jsonschema package). Outputs in outputs/: per-fixture results, the route confusion matrix, a log of every repair applied and metrics, and a report that states "fixture mode: no live generations evaluated".
Limitations
Fixture mode measures the validator on faults the author anticipated; real model faults may be different and more varied.
The silent-wrong rate on fixtures depends on how many semantic faults were authored; it is not a model error rate.
Rule-based injection detection is evadable by paraphrase; it is a tripwire, not a security boundary.
Production and monitoring
Put the validator in the request path with the schema version in every log line. Track schema-valid, repaired and human-review rates by source station and model version; alert on jumps. Repairs are logged and sampled for review. The real security boundary is downstream: extracted records cannot trigger passenger messages or refunds without a deterministic policy check and, for high-impact actions, human approval.
Interview questions
Schema-constrained decoding is available from the provider. Why do you still need validation and semantic checks?
Which repairs are safe to automate and which must never be automated? Give an example of a harmful repair.
How would you estimate the silent-wrong rate in production when you have no gold labels?
Postmortem and alternatives
The likely lesson is that syntax and schema faults are the easy part and semantic errors dominate risk. Discuss whether a smaller schema (fewer free-text fields) would have reduced injection surface more than any detector, and whether re-asking the model is worth its cost compared with routing to a person.
Reference implementation
Status: executed — outputs below are from the recorded run
r09 results - structured-output validation on recorded fixtures
Fixture mode: no live generations evaluated. Every output string scored here is a hand-authored template or a
deterministic fault injection applied to a synthetic gold record (file fixtures/fixtures_v1.json, seed 20240901).
These results describe the validator, not any language model. Numbers are copied from outputs/metrics.json written
by cd python && uv run python -m reference.r09_structured_output_validation.run.
| Item | Value |
|---|---|
| Fixture set | 100 synthetic notices, 182 labelled output fixtures: valid 25, syntax 26, schema 47, semantic 51, injection-followed 18, injection-ignored 15 |
| Split | dev 95 fixtures / test 87 fixtures, split by notice (dev 50 notices / test 50 notices) |
| Schema | domain.RECORD_SCHEMA v1.0.0, nine required fields, additionalProperties: false |
| Runtime | 0.2 s for scoring all four strategies on all 182 fixtures; standard library only (jsonschema is not used or added) |
Strategies: strict = parse and schema only (the baseline: anything that fails goes to a person); repair_only = plus the safe repairs; repair_plus_checks = plus source cross-checks, the several-flights rule and injection signals (the pipeline under test); aggressive = plus unsafe repairs (an ablation, never recommended).
Findings (all 182 fixtures unless stated)
- Schema validation catches every schema violation and raises no false alarm on this set. Of 78 fixtures that are
not strict-valid by construction (syntax and schema faults, plus 5 injection outputs that add a field or are not JSON),
the stdlib validator rejected 78 (100%, Wilson 95% interval 95.3% to 100%); of the 104 that are strict-valid by
construction it rejected none. The same holds on dev (38 of 38) and test (40 of 40). The stdlib validator also agrees
with the
jsonschemalibrary on every fixture output and on 400 random mutations when that library is installed (differential test; it was installed in this environment as a dependency of another tool). - Only 57.1% of the outputs are valid on the first strict parse; safe repair lifts it to 73.1%, and every repair was correct. Strict-valid 104 of 182; schema-valid after safe repair 133 of 182. 29 fixtures were repaired with 32 logged operations (9 enum normalisations, 7 code-fence strips, 6 trailing-comma removals, 5 prose strips, 3 boolean- string and 2 integer-string coercions). Harmful repairs (a correct value altered): 0. Invented values filled in: 0. Records accepted after repair that differ from gold: 0. These are properties of the 14 value-level repairs and 18 formatting repairs on this set, not a guarantee for unseen faults.
- Routing. Route accuracy against the expected route: strict 48.9% (89/182), repair_only 64.8%, repair_plus_checks 90.7% (165/182, Wilson 85.6% to 94.1%), aggressive 82.4%. Under the default pipeline 53 outputs (29.1%) go straight through, 29 (15.9%) are accepted after repair and 100 (54.9%) go to a person. Route confusion (expected by predicted): accept 38 / 0 / 2, accept-after-repair 0 / 29 / 0, human-review 15 / 0 / 98. Dev and test agree (89.5% and 91.9%).
- The silent-wrong rate is the number that matters, and validation alone leaves most of it. A schema-valid but wrong record that is accepted is "silent wrong". Strict: 60 of 104 accepted records (57.7%). Safe repair adds 29 records that are all correct, so the count stays at 60 and the denominator grows: 60 of 133 (45.1%). Source cross-checks and the injection signals cut it to 15 of 82 accepted records (18.3%, Wilson 11.4% to 28.0%; 8.2% of all fixtures). All 15 come from the four semantic fault types the rules cannot see by design (date equal to the issue date, delay taken from another number in the notice, rebooking flipped when the phrase is not in the lexicon, a vague cause made specific; 4, 4, 4 and 3 fixtures). Dev has 9 and test has 6.
- Cross-checks, not the schema, do the work against wrong values. 36 of the 51 semantic fixtures reach a person: 32 through a source check (flight, airport, date or delay absent from the notice, a type, cause or rebooking value that contradicts the wording, and so on) and the 4 two-flight notices through the several-flights rule. Per-field exact match among accepted records under the default pipeline: delay_minutes 95.1%, date 95.1%, rebooking_offered 95.1%, cause_category 96.3%, every other field 100%; under strict acceptance delay_minutes is 82.7%.
- Injected instructions. The output-side detector found 18 of 18 followed-instruction outputs (recall 1.00, Wilson
82.4% to 100%) with 2 false alarms (precision 0.90, 18 of 20). The two false alarms are the fixtures in which the
instruction asked for the value that was already true, so the output equals gold (labelled "ignored" by
construction); a detector cannot tell that from following. The input-side detector flagged all 33 notices with an
embedded instruction and none of the 62 others (the fixture set has no benign notice containing a link; a unit test
checks that a link without an instruction is not flagged on the input side). On test alone: output side 8 of 8 recall, 8 of 9 precision. Of the 18 followed
outputs, 13 are schema-valid: under the strict baseline those 13 would have reached automation. The default pipeline
routes all 18 to a person; 8 are caught by a source check (rebooking, delay or cause contradicts the notice) and the
injection signal, 5 by a schema/parse failure and the signal, and 5 (the e-mail and link requests that live in the
free-text
summary) by the injection signal alone. - Unsafe repairs are not an improvement. The aggressive strategy repairs 55 fixtures with 62 operations (39 field-
level). It sends 15 more fixtures to
accept-after-repair: 11 whose repaired record happens to equal gold (6 extra fields dropped, 3 unknown enum values mapped to "other" where gold was "other", 1 day-first date guess, 1 negative delay clipped to 0 where gold was 0) and 4 truncated outputs closed by the unsafe closer with the missing summary invented ("n/a"). Its silent-wrong count is 19 against 15, its route accuracy 82.4% against 90.7%, and 4 invented values were written. It did not alter a correct value on this set; its apparent successes depend on the fixture happening to agree with the default, which is the reason the default pipeline refuses them. The testtest_a_tempting_repair_is_blocked...shows a truncated record closed to"delay_minutes": 9(a cut-off 95), which only the cross-check then stops. - By template. The silent-wrong count per template (default pipeline) is 6 of 36 fixtures (template 1, the one with a "check-in closes N minutes" distractor), then 2 to 3 for the others, 0 for the four two-flight notices (all routed to a person by the several-flights rule).
What this does not prove
- It does not measure any model. No model produced these strings. Real model faults are more varied than the fault subtypes the author thought of, and the real rates of each are unknown.
- The 100% catch rate for schema violations is by construction and expected: the schema and the labels come from the same author, and the validator is deterministic. It says the validator implements the schema, not that the schema is the right contract.
- The silent-wrong rate (8.2% of fixtures, 18.3% of accepted) is a function of how many uncatchable semantic faults the author wrote (15 of 182). It is not an error rate for a model, and it should not be quoted as one.
- The injection detector's recall of 1.00 is circular: the same author wrote the seven instruction templates and the patterns. A paraphrased, encoded, translated or split instruction is not tested and would likely evade a pattern-based tripwire. It is not a security boundary: the boundary is that an extracted record cannot trigger a passenger message or refund without a deterministic policy check and, for high-impact actions, human approval.
- Dev and test are split by notice, but rules and fixtures have one author and no rule was changed after scoring. The test split therefore says nothing about generalisation to fault types the author did not anticipate; the record's ten fixtures written by someone else were not available.
- Notices follow five English templates (plus a two-flight form). Free-form, multilingual, spelled-out numbers ("ninety- five minutes"), look-alike characters beyond three fixtures and very long notices were not evaluated.
- Wilson intervals treat fixtures as independent; fixtures share notices (about 2 per notice), so the intervals are somewhat optimistic.
- The routing policy for an ignored injection (accept, log the flag) is a judgment; sending every flagged notice to a person would add the 13 accepted ignored-instruction fixtures (7% of all fixtures) to the human-review load.
Reproduce
cd python && uv run python -m reference.r09_structured_output_validation.run
cd python && uv run pytest tests/test_r09_structured_output_validation.pyRubric3 criteria
| Criterion | Meets | Exceeds |
|---|---|---|
| Schema and validation | Versioned schema with tests; all schema violations caught; semantic rules explicit and tested. | Source cross-checks catch schema-valid wrong values and are measured separately as silent-wrong reduction. |
| Repair safety | Every repair is logged and none fills a missing or wrong value. | Shows a tempting repair that would be harmful and the test that blocks it. |
| Honest evidence | Fixture-mode results labelled as validator measurements, not model quality. | Live mode run with a logged model and compared with fixture expectations. |
References5
- JSON Schema Specification
Schema language for required fields, enums and additionalProperties.
- Structured outputs (Claude Platform Docs)
Provider-side schema-constrained output and its scope.
- Structured model outputs (OpenAI API docs)
Second provider's schema enforcement for comparison.
- Not What You've Signed Up For: Compromising Real-World LLM-Integrated Applications with Indirect Prompt Injection
Instructions embedded in processed documents as the injection threat model.
- OWASP Top 10 for LLM Applications (2025 risk list)
Prompt injection and improper output handling risk categories (2025 list).