CLI tool
oxidecomputer/typify avatar
oxidecomputer/typify

Typify: JSON Schema compiled into Rust types

compiler from JSON Schema into idiomatic Rust types

909 stars116 forksRustApache-2.0

At a glance

What is it?
Typify is a Rust code generator that turns JSON Schema documents into structs, enums and collections with serde derives, usable from a cargo subcommand, a procedural macro, a build script or a builder API. It is written by Oxide Computer, and its README is unusually candid that JSON Schema is a validation language rather than a type system, so the mapping is a set of informed choices rather than a proof.
Who is it for?
Adopt Typify when a JSON Schema is the source of truth for an API or config format and you want Rust types with serde derives instead of a hand written model that drifts from the schema, since the generated output covers enums, flattened additional properties and known string formats.
Can I use it commercially?
Yes. Apache-2.0 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 4 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 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Why a schema compiler admits it is guessing

Typify compiles JSON Schema documents into Rust types, and the README opens with a paragraph that explains the whole design problem in three sentences.

JSON Schema is a constraint language designed for validation. As a result, it is not well suited, and is often seemingly hostile, to translation into constructive type systems. It allows expressions of arbitrary complexity with an infinity of ways to articulate a given set of constraints. As such, typify does its best to discern an appropriate interpretation, but it is far from perfect.

That admission is the most useful thing in the documentation, because it tells you what kind of tool this is. It is not a compiler for a type language, it is a translator between two incompatible worlds, and where the schema is ambiguous the generator picks a reading.

The consequence for you is that the output is a first draft with strong opinions, not a proof of equivalence with the schema. The project says so directly: if generation fails, does not compile or is generally lousy, file an issue and include the JSON Schema and the Rust output, and it is even more helpful if you can articulate the output you would ideally like to see.

Typify is written in Rust and comes from Oxide Computer, the hardware company behind the Oxide rack and the Propolis cloud. The licence is Apache-2.0.

Four ways to invoke it

The library can be used in several ways, and the choice is really about when the code is generated and where it lands.

The first is the cargo typify command, which is a cargo subcommand for generating types from the command line.

The second is a macro, import_types!("types.json"), which generates Rust types directly inside your program at compile time.

The third is a builder interface for generating Rust types in build.rs or in an xtask, which is what you want when the generation is part of a workspace build rather than a one-off step.

The fourth uses the builder functions to generate persistent files, for example when you are building API bindings and want the output checked into the repository so it can be reviewed.

The repository layout mirrors those modes. There is a cargo-typify/ directory for the subcommand, a typify-macro/ directory for the macro, typify-impl/ for the implementation, typify/ for the public library, typify-test/ for the test suite, and two example directories, example-build/ and example-macro/, that show the build script and macro approaches side by side. There is also an example.json at the root and an extension-schema.json that defines the extension mechanism discussed later.

The workspace uses resolver 2 and pins the three published crates at version 0.8.0, with the path dependencies pointing at the local directories.

How the schema constructs become Rust

The mapping rules are the substance of the tool, and the README documents each one.

Built-in types are straightforward. The nuance is picking the right integral type from the schema attributes, so a schema that specifies a maximum or minimum indicates the appropriate integral type to use. Strings with a known format become the appropriate Rust type, so a schema of type string with format uuid becomes a uuid::Uuid, which requires the uuid crate as a dependency.

Arrays become one of three things. An array with a fixed length matching a fixed list of item types becomes a Rust tuple. Otherwise the choice between Vec and HashSet comes down to the uniqueItems field being false or true respectively.

Objects become structs in general. If the schema defines no properties, you get a HashMap of String to T when additionalProperties specifies a T, and a HashMap of String to serde_json::Value otherwise.

Properties that are not in the required set become Option<T> with a serde default attribute applied. The exception is a type that already has a default value, such as a Vec, which simply gets the serde default attribute, so you will not see Option<Vec<T>>.

You can change the map type by calling with_map_type on the TypeSpaceSettings object and providing the full path, for example ::std::collections::BTreeMap or ::indexmap::IndexMap. There is a constraint: the custom map type only works if T is defined to generate a struct, otherwise typify generates code using a serde_json::Map of String to serde_json::Value instead.

oneOf, allOf and anyOf, in descending order of confidence

The three composition keywords are where the translator has to make judgement calls, and the README is refreshingly explicit about which ones are hard.

oneOf maps to a Rust enum, and Typify maps it to the various serde enum representations, which means the tagged and untagged flavours serde supports are both available depending on the schema.

allOf is handled by merging schemas. While most of the time typify tries to preserve and share type names, it cannot always do this when merging schemas. The stated consequence is that you may end up with fields replicated across types, and optimizing this generation is named as an area of active work.

anyOf is described as much trickier. It can be close to an enum, but where no particular variant might be canonical or unique for a given piece of data. Today, and with the word imprecisely used deliberately, they model these as structs with optional, flattened members, and this is called one of the weaker areas of code generation.

The README asks for issues describing example schemas and desired output, saying such issues are welcome and helpful. That is the right way to use a generator like this: bring it schemas you care about and check the output against what you would have written.

The additionalProperties keyword gets a long treatment of its own, because it decides whether unknown fields are an error, ignored, or captured. A value of false means no other properties are permitted, expressed with the serde deny_unknown_fields annotation. The absence of additionalProperties or a value of true mean any other property is permitted. In conjunction with other properties, absent or true means additional properties are ignored, while a non-boolean value produces a map field annotated with serde flatten.

x-rust-type and the round trip from Rust back to schema

The most interesting feature is the extension that lets a schema point back at a Rust type that already exists, which turns the tool from a one-way generator into a bridge between two ecosystems.

Schemas derived from Rust types may include an extension providing information about the original type.

json
{
  "type": "object",
  "properties": { .. },
  "x-rust-type": {
    "crate": "crate-o-types",
    "version": "1.0.0",
    "path": "crate_o_types::some_mod::SomeType"
  }
}

The extension carries the name of the crate, a Cargo-style version requirement spec, and the full path, which must start with the ident-converted name of the crate.

Each mode of using typify accepts a list of crates and versions to use. If you specify crate-o-types at 1.0.1, typify uses that crate's SomeType rather than generating one according to the schema.

The control surface for this differs per mode, and the default matters. The default is to ignore the x-rust-type annotations. The recommended method is to specify each crate and version you intend to use. You can additionally supply the wildcard version for crates, which may result in incompatibilities, or define a policy allowing all unknown crates, which may require adding dependencies for those crates.

The CLI exposes this with an option for unknown crates, and the README's own naming convention with crate-o-types and crate_o_types is an example of how a hyphenated crate name converts to a Rust identifier path.

Project state and how to try it

The repository is not archived and the last push was on 2026-10-19, and it publishes no GitHub releases, so the version history is tracked in the CHANGELOG.adoc file at the root and by the workspace pins.

The dependency list in the workspace shows what the generator itself is built on. It uses syn with the full feature set and quote and proc-macro2 for the macro side, prettyplease for formatting the generated code, heck for identifier conversion, and rustfmt-wrapper to run rustfmt over the output. serde, serde_json and schemars are the schema and serialisation crates, with serde_tokenstream for parsing serde attributes. The schema crate itself is at version 0.2.1.

The test tooling is worth noting. trybuild is included, which is the standard way to test that a macro produces compilable code, and expectorate is used for snapshot testing of the generated output, with regress also in the list. There is a clippy configuration file, an editorconfig, a release.toml for release automation and a rust-toolchain.toml pinning the toolchain.

The three example directories are the fastest way in. example-macro/ shows the macro form, example-build/ shows the build script form, and the example.json at the root is the schema they consume, so you can see the input and the output side by side before writing anything yourself.

Editorial conclusion

Adopt Typify when a JSON Schema is the source of truth for an API or config format and you want Rust types with serde derives instead of a hand written model that drifts from the schema, since the generated output covers enums, flattened additional properties and known string formats. Do not adopt it if your schema relies heavily on anyOf or on allOf merging, because the README calls those the weaker areas of the generator and says allOf cannot always preserve shared type names. Verify first that the generated code matches what you would have written by hand, especially around Option wrapping and additional properties, that your Rust edition and toolchain match the workspace, and that you check the workspace version pins, since the crate version is 0.8.0 and the project publishes no GitHub releases.

Frequently asked questions

What is Typify and what does it generate?

Typify compiles JSON Schema documents into Rust types. Objects become structs, oneOf becomes an enum using the serde enum representations, arrays become Vec, HashSet or tuples depending on length and uniqueItems, and properties that are not required become Option with a serde default attribute.

How do I use Typify in a Rust project?

Four ways are documented: the cargo typify command, the import_types!("types.json") macro, a builder interface for use in build.rs or an xtask, and the builder functions for generating persistent files such as API bindings checked into the repository.

What is the x-rust-type extension in Typify?

It is an extension that can appear in a schema derived from Rust types and carries the crate name, a Cargo-style version requirement and the full path of the original type. When you tell typify to use a given crate and version, it uses that type instead of generating a new one from the schema, and the default behaviour is to ignore these annotations.

Which JSON Schema constructs does Typify handle poorly?

anyOf is described as much trickier than oneOf and is currently modelled imprecisely as structs with optional flattened members, making it one of the weaker areas of code generation. allOf is handled by merging schemas, and typify cannot always preserve and share type names when merging, so fields can end up replicated across types.

Official sources

  1. Issues
  2. License: Apache-2.0
  3. oxidecomputer/typify on GitHub
  4. README
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.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/oxidecomputer-typify.svg)](https://hysenlabs.com/projects/oxidecomputer-typify)

Was this useful?