serde-zod
Generate zod
definitions from your JSON-serializable types in Rust.
Why
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
Install
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" }
Features
- structs ->
z.object()
- Optimized enum representation
- defer to
z.enum(["A", "B")
when a Rust enum contains onlyunit
variants (no sub-fields) - use
z.discriminatedUnion("tag", ...)
when attributeserde(tag = "kind")
is used - fall back to
z.union
if 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.
Basic Usage
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})
Output Types
z.enum()
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"})
z.discriminatedUnion
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`
}
z.union()
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 }})