Skip to content

Repository files navigation

unenforced

Finds the JSON Schema constraints no model provider will ever check.

Structured outputs and tool calling guarantee the shape of the data: which keys exist, their types, which are required. They do not guarantee the values. The two kinds of keyword sit side by side and look equally binding:

"email": { "type": "string", "format": "email", "minLength": 5 }
              ^ enforced        ^ decoration     ^ decoration

OpenAI's strict mode does not enforce minLength, maxLength, pattern or format — they are value-level constraints, and the token-level machinery that guarantees the shape cannot check them. It rejects default outright. Anthropic is plainer still: "The strict parameter is currently ignored for tool definitions. Claude will make a best effort to provide valid arguments, but does not guarantee schema compliance."

So the model hands you "x" for a field declared minLength: 5. The request succeeds. And the code that reads it was written by somebody who read the schema and reasonably concluded the field was validated — which is why the check so often is not repeated downstream.

$ npx unenforced .

3 schemas · 12 constraints nothing checks
                                 0 of 3 carry no decoration

error   keyword-rejected-by-strict-mode response-format.json:5
  1 keyword is rejected by strict mode
    extraction: published — default: unknown

warning constraint-not-enforced tools.json:4
  11 constraints in these schemas are never checked
  fix: Validate these fields in the code that receives the arguments — with the
       same schema, or with zod or pydantic — and keep the keywords for
       documentation.
    create_invoice: email — format: email
    create_invoice: email — minLength: 5
    create_invoice: reference — pattern: ^INV-[0-9]{6}$
    run_query: sql — pattern: ^SELECT

Install

npx unenforced .                  # no install
npm install --save-dev unenforced

Node 20.10 or newer. Zero runtime dependencies. No model is called, no schema is compiled, nothing is executed.

Quick start

unenforced .                             # scan the repository
unenforced . --verbose                   # list every unchecked constraint
unenforced . --target openai-strict      # assume a target where none is stated
unenforced . --json                      # machine-readable

Exit code 1 when something at or above the threshold is found, 0 when clean, 2 on bad usage.

Options

Flag Meaning
--target <api> openai-strict, openai-tools or anthropic-tools, assumed when a definition does not say
--json JSON report on stdout, verdict on stderr
--verbose List every constraint nothing will check
--fail-on <level> error (default), warning, or info
-h, --help / -v, --version

What it checks

Rule Severity What it means
keyword-rejected-by-strict-mode error default under strict mode. The request fails, at run time.
strict-mode-will-be-rejected error strict: true declared and the schema breaks its rules.
constraint-not-enforced warning format, pattern, minLength, minimum — decoration where validation should be.
property-has-no-description warning The model's only instruction about a field is missing.
strict-mode-not-available info What stands between this schema and a real shape guarantee.

Full reasoning and the fix for each is in docs/rules.md.

Where it looks

Definitions are found by shape, anywhere in a JSON document, because that is what the ecosystem looks like:

{ "tools": [ { "name": …, "input_schema": … } ] }        Anthropic
{ "tools": [ { "type": "function", "function": … } ] }   OpenAI
{ "mcpServers": { "db": { "tools": [ … ] } } }           an MCP manifest
{ "search": { "description": …, "parameters": … } }      SDK map-of-tools

The last two found nothing in 0.1.0. MCP in particular is where a growing share of agent tools are now defined.

Enforced, ignored, rejected

Keyword What happens
type, enum, const, required, properties, items, anyOf Enforced. These shape generation.
minLength, maxLength, pattern, format, minimum, maximum, minItems, uniqueItems Ignored. No provider checks them.
default Rejected by OpenAI strict mode. The request fails.

The middle row is the one this tool exists for. Nothing in a schema, a response, or an error message distinguishes those keywords from the ones above them.

Why the headline finding is a warning, not an error

Nothing is broken today. The schema is legal, the request succeeds, and the model usually does what the constraint says — models are good at following a pattern they can see. The finding is that a guarantee people are relying on is not one, and it fails on the day the model has an off distribution or somebody changes the prompt.

Calling that an error would be overstating a risk in order to be heard, which is the habit this tool is built against.


Try it

Two fixtures ship with the repo: the same three schemas — an Anthropic tool, an OpenAI structured output, and an MCP server manifest — written two ways.

git clone https://github.com/hamodywe/unenforced && cd unenforced
node src/cli.ts examples/decorated   # twelve unchecked constraints, exit 1
node src/cli.ts examples/checked     # silent, exit 0

examples/checked is not a schema with the constraints deleted and nothing put in their place. Every requirement moved into the description — where the model actually reads it — the optional field became a nullable required one so strict mode can hold, and the enum stayed exactly where it was, because an enum is enforced. It says more than the decorated one, in the place that works.

Limitations

Stated plainly, because a tool that overstates its coverage is worse than no tool.

  • Only schemas in JSON are read. A tool defined inline in TypeScript or Python, or built by zod or pydantic at import time, is not seen — extracting it means evaluating the module, and this tool executes nothing from a repository. When source files mention a schema and no JSON is found, the report says so rather than reporting a clean scan.
  • The target is inferred, not known. input_schema means Anthropic and strict: true means OpenAI strict; anything else is a guess unless you pass --target. The value-level finding holds on every target, which is why it is the headline.
  • Provider limits change. Only the stable, load-bearing ones are encoded — nesting depth, property count, closed objects, all-required. A number that changes quarterly would make this tool confidently wrong on somebody's repository the week after.
  • It does not check whether your code validates. It tells you which constraints are unenforced; whether the handler repeats them is a question about your code, not your schema.
  • $ref is not resolved. A schema assembled through references is walked as written, so a constraint behind a $ref to another file is not seen.

FAQ

So should I delete format and pattern? No. Keep them — they document intent, and some client libraries do validate against them. The point is to stop treating them as the validation, and to say the same thing in the description, which is the only text the model reads.

Does this mean structured outputs are unreliable? The opposite. Strict mode's shape guarantee is strong and worth having, which is why the tool reports what stands between your schema and it. The mistake is assuming the guarantee extends to values, and that assumption is invited by the schema format itself.

We validate with zod after parsing. Then you are doing exactly what this recommends, and the findings are a list of the constraints that validation needs to cover.

Will it change my files? No. It reads, it reports, it exits. There is no write path in the codebase.

Roadmap

See ROADMAP.md.

Contributing

See CONTRIBUTING.md. A schema this tool judged wrongly — especially a keyword a provider does enforce — is the most useful thing you can send.

License

MIT

About

Finds the JSON Schema constraints no model provider will ever check — format, pattern and minLength standing exactly where validation should be.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages