jiter is a JSON parser with three ways to read a document
Fast iterable JSON parser.
At a glance
- What is it?
- jiter is a Rust JSON parser from the Pydantic project that offers an enum for whole documents, a pull iterator for streaming, and a bridge that parses JSON straight into Python objects, with benchmark tables showing it ahead of serde_json on most shapes. It suits a Python project that parses JSON on a hot path, and it is written to be embedded rather than to serve as a general purpose library.
- Who is it for?
- Adopt jiter when you parse JSON on a hot path in a Python service and want a parser that goes straight to Python objects, or when you need to stream a large document without holding it all in memory. Do not adopt it as a general replacement for serde_json in Rust, because the iterator interface requires you to know the schema in advance, which is a constraint the standard deserializer does not impose.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 2 days ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Three interfaces for three different shapes of work
jiter is described as a fast iterable JSON parser, and it has three interfaces. JsonValue is an enum representing JSON data, so the whole document becomes a tree you can walk. Jiter is an iterator over JSON data, so you pull values out one at a time and never hold the whole thing in memory. PythonParse parses a JSON string into a Python object, which is the path most Python users take because it skips the intermediate tree entirely. The choice between them is a real decision rather than a matter of taste. If you know the shape of the document, the iterator is fastest and allocates least, but the README is explicit that to use Jiter you need to know what schema you are expecting, and the example walks an object key by key with next_object, next_str, next_key, next_int, next_array and a known_str that assumes the value type you just observed. If you do not know the shape, JsonValue is the interface, and if you want Python objects rather than Rust values, PythonParse is it.
The value interface, and what it gives you
The JsonValue example is a complete program and the printed output shows the shape of the enum. The document has a name, an age and a list of phones, and the parse call takes the bytes and a boolean, which is the flag for allowing partial or lenient parsing, and the result is unwrapped. The printed value is an Object variant holding a map, and the entries are a Str for the name, an Int for the age and an Array of Str for the phones. The design point is that the numbers are typed rather than collapsed into a single float, so an integer stays an integer, which matters when the value is going to be handed to something that distinguishes the two. The documented example links to the JsonValue docs for the full set of variants, and the enum is what you reach for when the document is irregular or when you need to inspect it rather than consume it.
The parse call is a single expression:
let json_value = JsonValue::parse(json_data.as_bytes(), true).unwrap();
println!("{:#?}", json_value);The iterator, where knowing the schema pays
The Jiter example is the fastest way to understand what this library is for, and it is a sequence of assertions rather than a parse call. You construct a Jiter over the bytes and then assert that the next object key is name, that the next string is John Doe, that the next key is age, and that the next integer is a NumberInt::Int of 43. Then the next key is phones, the next array is a Peek::String, and you use known_str to read the value knowing from the peek that it is a string, then array_step to advance, and known_str again for the second number. The last array_step returns None because the array is finished, the next_key returns None because the object is finished, and you finish by calling finish(), which checks there is nothing else in the input. The known_str and array_step pair is the clever part, since a peek tells you the type of the next element and the known_ call then skips re-checking it, which is where the speed comes from when the schema is known.
The benchmark table, and the caveat that comes with it
The README opens the benchmark section with a warning that there are lies, damned lies and benchmarks, and then makes a specific methodological point rather than a general one. The serde-json benchmarks use serde_json::Value, which is significantly slower than deserializing to a string, so the comparison is not against the fastest serde path. The table then reports jiter iter, jiter value and serde value for a wide range of shapes, with a ratio column. The pattern is fairly consistent. On strings, the x100 case is 8ns against 40ns, a 4.4 times difference, and a string array is 466ns against 3.0 microseconds, 4.0 times. On numbers, a long significand floats case is 12.1 microseconds against 94.7, 6.8 times, and a big case is 2.93ms against 20.32ms, 5.1 times. On whole documents, json_cases_all is 22.15ms against 90.38ms, 4.1 times. The exception is the pass2 case, where the jiter value path is slower than serde at 0.7 times, so there is at least one shape where the comparison goes the other way.
What the benchmarks leave out
A few of the rows are more informative than the headline numbers, and reading them tells you what the parser is good at. The json_cases_error row shows 2.38ms against 11.87ms, a 5.0 times difference, which means the error path is fast as well, and that matters because a parser that is quick on success and slow on malformed input is a liability in production where the malformed input is the attack. The json_cases_whitespace row at 2.7 times shows that documents with lots of whitespace are still handled well. The json_cases_non-ascii row at 1.4 times is the smallest of the document cases, so the advantage narrows on non-ASCII text, which is a useful caution if you parse international content. And the iter column has dashes in most rows, meaning the iterator interface only applies to the cases where the schema is known, so the two interfaces are not measured on the same work and the ratios between them are not directly comparable.
The Python binding and the build system around it
The Python side is a separate crate in the workspace, crates/jiter-python, built with pyo3 and pyo3-build-config at version 0.30.1, and the Python project file is a uv workspace with that one member and the package marked as not a package itself. The Makefile shows the developer loop in detail. Formatting and linting for Rust is cargo fmt and cargo clippy with warnings denied, plus a second clippy pass targeting wasm32-wasip1 for the jiter package so the WebAssembly build stays clean, and cargo doc. The Python side uses uv with a linting group containing ruff, and there is a separate group for markdown formatting with mdformat. The Python development loop is maturin develop in uv mode, with a release variant and a pgo variant that adds profile guided optimisation. The Python test group has pytest with pytest-pretty and dirty-equals, and a bench group with orjson and ujson as comparison parsers. The Cargo workspace excludes the fuzz crate and a Rust json-cases checkout, and the MSRV is 1.88 with a comment that it should match pydantic-core, which is the tell that this library is versioned in lockstep with the rest of the Pydantic stack.
Where it sits in the Pydantic stack
The README closes with a section pointing at the wider Pydantic Stack, framed as everything needed to ship production-grade AI agents. It lists Pydantic AI, a type-safe agent framework, Pydantic Logfire, an observability tool, and the Logfire AI Gateway, a unified LLM proxy. The relevance of that list for jiter is the Python path, because jiter exists in the first place as the JSON engine under Pydantic's own validation and parsing, and the standalone crate is the same code exposed for other users. The repository layout reflects that structure, with the crates/ directory holding the Rust crates, a pyproject.toml and uv.lock for the Python side, a Makefile, a .pre-commit-config.yaml, a .rustfmt.toml and a .yamlfmt.yaml, and codecov.yml for coverage reporting. The release history shows v0.17.0 in September 2026, v0.16.0 in June 2026 and v0.15.0 in May 2026, with the Cargo manifest already at 0.18.0, and the last push was on 2026-10-15.
Editorial conclusion
Adopt jiter when you parse JSON on a hot path in a Python service and want a parser that goes straight to Python objects, or when you need to stream a large document without holding it all in memory. Do not adopt it as a general replacement for serde_json in Rust, because the iterator interface requires you to know the schema in advance, which is a constraint the standard deserializer does not impose. Verify first that you are on Python 3.10 or newer, that the document shapes you parse match the ones in the benchmark table rather than the average, and that the Python binding is available for your platform.
Frequently asked questions
What are the three interfaces jiter offers?
JsonValue is an enum representing JSON data, Jiter is an iterator over JSON data, and PythonParse parses a JSON string into a Python object. The iterator requires you to know the schema you are expecting, which is why the other two exist.
How does jiter compare with serde_json?
On the reported benchmarks jiter is faster on most shapes, with json_cases_all at 22.15ms against 90.38ms for serde, a 4.1 times difference, and a floats array at 6.1 times. The README notes the serde comparison uses serde_json::Value rather than deserializing to a string, and the pass2 case is the one where the jiter value path is slower.
What do the known_str and array_step calls do in jiter?
They let you exploit a known schema. The next_array call returns a Peek describing the type of the next element, and once you have asserted that type you can call known_str to read the value without re-checking it, with array_step advancing the iterator. The example ends with finish, which checks there is nothing else in the input.
How do you work on the jiter Python binding?
The Makefile drives it with uv and maturin, where python-install syncs all groups and packages, python-dev runs maturin develop in uv mode against crates/jiter-python/Cargo.toml, python-test runs the pytest suite in crates/jiter-python/tests, and there are release and profile guided optimisation variants of the develop target. The Python tests need Python 3.10 or newer.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/pydantic-jiter)Was this useful?