JSON Schema for agents

Tool calls and structured output, checked as models write them. flowschema compiles a schema in about a microsecond, checks a call against it with no allocation, and says what is wrong in words a model can fix its answer from.

cargo add flowschema --git https://github.com/localixai/flowschema

A Rust crate · MIT · two dependencies · Get started

What an agent does with JSON

Six things it is made for

Tool calls

A call that does not fit is not run

Compile a tool’s parameters, check every call the model writes, and send what is wrong back as the call’s result. Each message says where the value is, the rule, and what the data has.

the call the model wrote{"path": 7, "limit": 0, "force": true} what goes back to itdata has "force", which the schema does not allowdata.limit must be >= 1 (is 0)data.path must be string (is integer)

Per request

A schema that comes with the request

Where every request brings its own tools, compile the schema, check, and drop it. Schema::new reads the document where it is and copies nothing, so there is no cache to key, to bound or to clear.

compiled, one answer checkedflowschemajsonschemaa tool call508 ns3.89 µs7.6×a structured answer1.16 µs6.31 µs5.4×nested models2.46 µs10.34 µs4.2× Both built for speed, on an Apple M5; the jsonschema crate 0.60.

Providers

What a strict mode takes, and what is still yours

OpenAI, Anthropic and Gemini each hold a model to their own part of JSON Schema. fit gives the schema a provider takes and says every change; the answer is still checked against the schema as it was written.

fit(&schema, Provider::Anthropic).changesschema.properties.limit: `minimum` is not held: taken outschema: `additionalProperties` set to false, as the provider asks the answer, against the schema as writtendata.limit must be >= 1 (is 0)

Streams

Cut off, still arriving, or written as a string

whole gives what a reply the output limit stopped wrote whole. may_match says whether a call still streaming can still end in a match. mend reads "30" as the integer the schema asks for, and says so.

whole: {"path": "src/lib.rs", "lines": [1, 2{"lines":[1],"path":"src/lib.rs"} may_match: {"mode": "xfalse   no option begins so mend: {"limit": "30"}data.limit: "30" read as 30

Whole

All of JSON Schema, and what is not checked is said

Drafts 4 to 2020-12, every keyword and applicator, $ref and formats: every required test of the official suite passes. A schema that asks for what is not checked is never taken as stricter than it is: unchecked names it.

official test suite7,675 of 7,675real LLM answers judged as labelled99.98%schemas of MaskBench compiled11,306 of 11,306panics on any of them0

Small

Two dependencies, no allocation on a check

It adds 309 KB to a release binary and two crates to a build: serde_json, which you have, and regex-lite. No unsafe, no I/O, nothing fetched, and every pattern runs in time linear in its text.

flowschemajsonschemaadded to a binary309 KB1,884 KBcrates added to a build257allocations on a check0up to 7

Documentation

7 pages →

Start checking

Free · MIT license

  1. Add it

    cargo add flowschema --git https://github.com/localixai/flowschema, beside serde_json. Rust 1.89 or later.

  2. Compile the schema

    Schema::new(&schema): a tool’s parameters or an answer’s shape, once for many checks or from the request that brings it.

  3. Check, and send back

    matches says whether the data fits; mismatches writes what to send the model so it fixes its answer.