valjson
Diagnostics for structured JSON output from LLMs: extract, validate, fix, score per-field accuracy, and gate on confidence. Most commands need no model.
Fine-tuning your LLM for JSON? Your aggregate metrics might be hiding per-field regressions.
pip install valjson
What it does
valjson is the observability layer for structured JSON output. It meets you wherever you are:
| You have | Command | Needs model? |
|---|---|---|
| Messy text with JSON | valjson --extract --data output.txt |
No |
| JSON + schema | valjson --validate --schema s.json --data output.jsonl |
No |
| Broken JSON + schema | valjson --fix --schema s.json --data output.jsonl |
No |
| JSON + schema | valjson --anatomy --schema s.json --data output.jsonl |
No |
| JSON + schema + gold | valjson --compare --schema s.json --data output.jsonl --gold truth.jsonl |
No |
| Two output sets | valjson --diff --schema s.json --data a.jsonl --data2 b.jsonl |
No |
| Per-field probabilities | valjson --gate --data probs.jsonl |
No |
| Model + checkpoint | valjson --checkpoint lora/ --schema s.json --data test.jsonl |
Yes |
The problem
Standard fine-tuning + grammar-constrained decoding produces valid JSON. Aggregate loss improves. But:
STRUCTURAL 5.33 -> 0.00 -100% OK
KEY 0.47 -> 0.00 -100% OK
BOOLEAN 0.46 -> 1.05 +130% !! REGRESSION
TOTAL 0.55 -> 0.17 -69%
Aggregate loss improved 69%. Boolean prediction got 130% worse. valjson catches this.
Per-field accuracy vs gold
When you have human-labeled gold JSONs, --compare gives you per-field accuracy
without needing a model — role-aware, so a blown free-text field does not drown
the signal on the constrained fields you actually care about:
valjson --compare \
--schema schema.json \
--data generated.jsonl \
--gold gold.jsonl \
--ignore-role STRING,ARRAY
--match-by <key>— pair records by ID (wrapper-first lookup), not line order.--ignore-role STRING,ARRAY— focus on BOOLEAN / ENUM / NUMBER fields where exact equality is meaningful. Unmatched IDs are reported separately.
Evidential gating: abstain when the model is unsure
When the model returns a probability distribution over allowed values for each
constrained field, --gate decides per record whether to commit, abstain,
or reject based on the margin between the top two values:
valjson --gate \
--data probs.jsonl \
--margin-threshold 0.30
Input format (one record per line):
{"id": "rec-001",
"probs": {
"refundable": {"True": 0.553, "False": 0.447},
"status": {"submitted": 0.9, "pending": 0.1, "draft": 0.0}
}}
A field's margin = top_prob − second_prob. If margin ≥ threshold, the
field is committed; otherwise it abstains. Per-field abstention rates are
reported — fields with persistently high abstention are diagnostic of
underspecified training targets (the regression pattern this paper documents).
Quick start
See QUICK_START.md for a hands-on walkthrough from messy output to full analysis.
Python API
from valjson import analyze
report = analyze(
model_name="Qwen/Qwen2.5-0.5B-Instruct",
checkpoint="my_lora/",
schema="schema.json",
data="test.jsonl",
)
print(report)
if report.regressions:
print(f"REGRESSIONS: {[r.role for r in report.regressions]}")
Exit code is 1 if regressions are detected. Use in CI/CD.
Grammar Roles
| Role | Description | Examples |
|---|---|---|
| STRUCTURAL | JSON syntax | { } [ ] : , |
| QUOTE | String delimiters | " |
| KEY | Object key characters | city, cuisine |
| ENUM_VALUE | Categorical values | Italian, Economy |
| BOOLEAN | Boolean strings | True, False |
| NUMBER | Numeric characters | 42, 3.14 |
| FREE_TEXT | Non-categorical content | names, addresses |
| WHITESPACE | Formatting | spaces, newlines |
Links
- Quick Start Tutorial
- Fixing JSON Fine-Tuning — mitigations, services
- Paper — "Valid JSON, Wrong Answer" (2026), DOI 10.5281/zenodo.20075999
License
MIT
Metadata
Release files for valjson 2.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| valjson-2.0.1.tar.gz | 56.2 kB | Without provenance | Details for valjson-2.0.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| valjson-2.0.1-py3-none-any.whl | Python 3 | none | any | Details for valjson-2.0.1-py3-none-any.whl |
Total release size: 121.1 kB
Release files / valjson-2.0.1.tar.gz
| Download URL | valjson-2.0.1.tar.gz |
|---|---|
| Size | 56.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
dfea541992447c142a69e2811fc622fd58a5e70df3892a9314d372b6e266468d
|
|
BLAKE2b-256 checksum How to use checksums |
2ce6e146f6a1c2b6c0e99a8f80378dd3b3a8073c6a7d6da6a12f9027eb3cfb55
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.4
|
Release files / valjson-2.0.1-py3-none-any.whl
| Download URL | valjson-2.0.1-py3-none-any.whl |
|---|---|
| Size | 64.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c7909930d645917bda375c576c4103baa3e2f5850c602521d204b25483158c34
|
|
BLAKE2b-256 checksum How to use checksums |
ce445c578c212449b8bfa23ef6ed097afb539cb5a4cce3e486756906abce52b9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.4
|
Security for valjson 2.0.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.