Generate zod definitions from your JSON-serializable types in Rust.
This library was created whilst building a Tauri App where the architecture encourages heavy use of JSON serializable messaging.
Having those message structures described in Rust structs/enums was fantastic, but losing the type information on the frontend was a shame - so I built this attribute macro to solve that problem 💪😎
It's not on crates.io yet, it will be soon.
In the meantime if you want to try it out, you can reference the git repo in your Cargo.toml
serde_zod = { git = "https://github.com/shakyShane/serde-zod.git#main" }- structs ->
z.object() - Optimized enum representation
- defer to
z.enum(["A", "B")when a Rust enum contains onlyunitvariants (no sub-fields) - use
z.discriminatedUnion("tag", ...)when attributeserde(tag = "kind")is used - fall back to
z.unionif fields are mixed
- defer to
- array subtype via
Vec<T> - optional types
Option<T> - HashMap/BTreeMap
- Set/BTreeSet
- serde rename_all
- serde rename_field
- document all available output types
| rust | zod |
|---|---|
| enum with only unit variants | z.enum([...]) |
| enum with "tagged" variants | z.discriminatedUnion("tag", ...) |
| enum with mixed variants | z.union([...]) |
| String | z.string() |
| usize|u8|u16|f32|f64 etc (numbers) | z.number() |
| Option | z.string().optional() |
| Struct/Enum fields | z.object({ ... }) |
See the tests for more examples, or the Typescript output to see what it generates.
Add the #[serde_zod::codegen] attribute above any existing Rust struct or enum where you
already have #[derive(serde::Serialize)] or #[derive(serde::Deserialize)]
#[serde_zod::codegen]
#[derive(serde::Serialize)]
pub struct Person {
age: u8,
}With that, you can then create a binary application (alongside your lib, for example) to output the zod definitions
fn main() {
let lines = vec![
Person::print_imports(), // ⬅️ only needed once
Person::codegen(),
];
fs::write("./app/types.ts", lines.join("\n")).expect("hooray!");
}output
import z from "zod"
export const Person =
z.object({
age: z.number()
})
// ✅ usage
const person = Person.parse({age: 21})When you have a Rust enum with only unit variants - meaning all variants are 'bare' or 'without nested fields', then serde-zod will print a simple z.enum(["A", "B"])
input
#[serde_zod::codegen]
#[derive(Debug, Clone, serde::Serialize)]
pub enum UnitOnlyEnum {
Stop,
Toggle,
}
#[serde_zod::codegen]
#[derive(serde::Serialize)]
pub struct State {
control: UnitOnlyEnum,
}output
import z from "zod"
export const UnitOnlyEnum =
z.enum([
"Stop",
"Toggle",
])
export const State =
z.object({
control: UnitOnlyEnum,
})
// usage
// ❌ Invalid enum value. Expected 'Stop' | 'Toggle', received 'oops'
const msg = State.parse({control: "oops"})
// ✅ Both valid
const msg1 = State.parse({control: "Stop"})
const msg2 = State.parse({control: "Toggle"})When you use #[serde(tag="<tag>")] on a Rust enum, it can be represented by Typescript as a 'discriminated union'. So,
when serde-zod notices tag=<value>, it will generate the optimized zod definitions. These offer the best-in-class
type inference when used in Typescript
input
#[serde_zod::codegen]
#[derive(serde::Serialize)]
#[serde(tag = "kind")]
pub enum Control {
Start { time: u32 },
Stop,
Toggle,
}output
import z from "zod"
export const Control =
z.discriminatedUnion("kind", [
z.object({
kind: z.literal("Start"),
time: z.number(),
}),
z.object({
kind: z.literal("Stop"),
}),
z.object({
kind: z.literal("Toggle"),
}),
])
// this usage show the type narrowing in action
const message = Control.parse({ kind: "Start", time: 10 });
if (message.kind === "Start") {
console.log(message.time) // ✅ 😍 Type-safe property access here on `time`
}This is the most flexible type, but also gives the least type information and produces the most unrelated errors (you get an errorr for each unmatched enum variant)
input
#[serde_zod::codegen]
#[derive(serde::Serialize)]
pub enum MixedEnum {
One,
Two(String),
Three { temp: usize },
}output
import z from "zod"
export const MixedEnum =
z.union([
z.literal("One"),
z.object({
Two: z.string(),
}),
z.object({
Three: z.object({
temp: z.number(),
}),
}),
])
// ✅ all of these are valid, but don't product great type information
MixedEnum.parse("One")
MixedEnum.parse({Two: "hello...."})
MixedEnum.parse({Three: { temp: 3 }})