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
npx unenforced . # no install
npm install --save-dev unenforcedNode 20.10 or newer. Zero runtime dependencies. No model is called, no schema is compiled, nothing is executed.
unenforced . # scan the repository
unenforced . --verbose # list every unchecked constraint
unenforced . --target openai-strict # assume a target where none is stated
unenforced . --json # machine-readableExit code 1 when something at or above the threshold is found, 0 when clean, 2 on bad usage.
| 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 |
| 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.
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.
| 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.
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.
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 0examples/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.
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_schemameans Anthropic andstrict: truemeans 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.
$refis not resolved. A schema assembled through references is walked as written, so a constraint behind a$refto another file is not seen.
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.
See ROADMAP.md.
See CONTRIBUTING.md. A schema this tool judged wrongly — especially a keyword a provider does enforce — is the most useful thing you can send.