Thanks to visit codestin.com
Credit goes to github.com

Skip to content

shakyShane/serde-zod

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

29 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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 only unit variants (no sub-fields)
    • use z.discriminatedUnion("tag", ...) when attribute serde(tag = "kind") is used
    • fall back to z.union if fields are mixed
  • 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 }})

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published