Generating Rust from JSON Typedef schemas (2023)

JSON Type Definition, aka RFC 8927, is aneasy-to-learn, standardized way to define a schema for JSON data. You can useJSON Typedef to portably validate data across programming languages, createdummy data, generate code, and more.

This article is about how you can use JSON Typedef to generate Rust code fromschemas. If you’re interested in generating code in other languages, see thisarticle on jtd-codegen. The rest of this article focuseson using jtd-codegen with Rust in particular.

Generating Rust with jtd-codegen

As a prerequisite, you need to first install jtd-codegen. Installationinstructions are available here.

You can generate Rust with jtd-codegen using the --rust-out option, whosevalue must be a directory that jtd-codegen can generate code into.

For example, if you have this schema in schemas/user.jtd.json:

{ "properties": { "id": { "type": "string" }, "createdAt": { "type": "timestamp" }, "karma": { "type": "int32" }, "isAdmin": { "type": "boolean" } }}

Then you can generate Rust code into the src/user directory by running:

jtd-codegen schemas/user.jtd.json --rust-out src/user

Which will output something like:

📝 Writing Rust code to: src/user📦 Generated Rust code.📦 Root schema converted into type: User

And you should see code along these lines in src/user/mod.rs:

use chrono::{DateTime, FixedOffset};use serde::{Deserialize, Serialize};#[derive(Serialize, Deserialize)]pub struct User { #[serde(rename = "createdAt")] pub createdAt: DateTime<FixedOffset>, #[serde(rename = "id")] pub id: String, #[serde(rename = "isAdmin")] pub isAdmin: bool, #[serde(rename = "karma")] pub karma: i32,}
(Video) What is JSON Schema

Note: at the time of writing, generated code is not always formatted in apretty way. If you require pretty-formatted code, it’s recommended that you usea code formatter on jtd-codegen-generated code.

Using generated Rust code

jtd-codegen will always output code into a mod.rs inside the directory youspecify with --rust-out. In the previous example, we outputted code intosrc/user, so we can import it like so:

use user::User;

The generated Rust code is meant to be used with the serde_jsoncrate. To use the generated types, pass themas a parameter to serde_json::from_str / serde_json::to_string (or whatevervariant of those methods is relevant to you).

For example:

// To read in JSON, do something like:let input_json = "...";let user: User = serde_json::from_str(input_json)?;// To write out JSON, do something like:serde_json::to_string(&user)?;

In the example above, we directly serde_json::from_str unvalidated input intothe jtd-codegen-generated type. In many cases, this is perfectly fine to do.However, there are is a caveat when doing this: the errors serde_json producesare Rust-specific and low-level.

You can address this issue (if it is an issue for your use-case) by firstvalidating the input against a JTD validation implementation, such as the jtdcrate. What you would do is:

  1. Parse the input into a serde_json::Value, rather than the generated type.
  2. Validate that the parsed serde_json::Value is valid against the schema yougenerated your types from. If there are validation errors, you can returnthose, because JTD validation errors are standardized andplatform-independent.
  3. If the input is valid, then parse the Value into your generated type. Youcan do this using serde_json::from_value.

This solution lets you produce portable validation errors. It does, however,come at the cost of requiring you to process the input JSON document tree twice.

Customizing Rust output

Rust code generation supports the following metadata properties shared acrossall languages supported by jtd-codegen:

  • description customizes the documentation comment to put on a type orproperty in generated code. For example, this schema:

    { "metadata": { "description": "A user in our system" }, "properties": { "name": { "metadata": { "description": "The user's name" }, "type": "string" }, "isAdmin": { "metadata": { "description": "Whether the user is an admin" }, "type": "boolean" } }}

    Generates into:

    use serde::{Deserialize, Serialize};/// A user in our system#[derive(Serialize, Deserialize)]pub struct User { /// Whether the user is an admin #[serde(rename = "isAdmin")] pub isAdmin: bool, /// The user's name #[serde(rename = "name")] pub name: String,}
    (Video) Generating TypeScript Types for GraphQL Schema, Queries and Mutations Using GraphQL Code Generator
  • enumDescription is like description, but for the members of an enum. Thekeys of enumDescription should correspond to the values in the schema’senum, and the values should be descriptions for those values. For example,this schema:

    { "metadata": { "enumDescription": { "PENDING": "The job is waiting to be processed.", "IN_PROGRESS": "The job is being processed.", "DONE": "The job has been processed." } }, "enum": ["PENDING", "IN_PROGRESS", "DONE"]}

    Generates into:

    use serde::{Deserialize, Serialize};#[derive(Serialize, Deserialize)]pub enum Status { /// The job has been processed. #[serde(rename = "DONE")] Done, /// The job is being processed. #[serde(rename = "IN_PROGRESS")] InProgress, /// The job is waiting to be processed. #[serde(rename = "PENDING")] Pending,}

Additionally, Rust code generation supports the following Rust-specific options:

Generated Rust code

This section details the sort of Rust code that jtd-codegen will generate.

Code generated from “Empty” schemas

“Empty” schemas will be converted into aRust Option<serde_json::Value>:

{}
(Video) Introducing OpenAPI Generator

Generates into:

use serde_json::{Value};pub type Empty = Option<Value>;

Code generated from “Ref” schemas

“Ref” schemas will be converted into areference to the definition being referred to:

{ "definitions": { "example": { "type": "string" } }, "ref": "example"}

Generates into:

pub type Ref = Example;pub type Example = String;

Code generated from “Type” schemas

“Type” schemas will be converted intothe following types:

JSON Typedef typeRust type
booleanbool
stringString
timestampchrono::DateTime<chrono::FixedOffset>
float32f32
float64f64
int8i8
uint8u8
int16i16
uint16u16
int32i32
uint32u32

For example,

{ "properties": { "boolean": { "type": "boolean" }, "string": { "type": "string" }, "timestamp": { "type": "timestamp" }, "float32": { "type": "float32" }, "float64": { "type": "float64" }, "int8": { "type": "int8" }, "uint8": { "type": "uint8" }, "int16": { "type": "int16" }, "uint16": { "type": "uint16" }, "int32": { "type": "int32" }, "uint32": { "type": "uint32" } }}

Generates into:

use chrono::{DateTime, FixedOffset};use serde::{Deserialize, Serialize};#[derive(Serialize, Deserialize)]pub struct Type { #[serde(rename = "boolean")] pub boolean: bool, #[serde(rename = "float32")] pub float32: f32, #[serde(rename = "float64")] pub float64: f64, #[serde(rename = "int16")] pub int16: i16, #[serde(rename = "int32")] pub int32: i32, #[serde(rename = "int8")] pub int8: i8, #[serde(rename = "string")] pub string: String, #[serde(rename = "timestamp")] pub timestamp: DateTime<FixedOffset>, #[serde(rename = "uint16")] pub uint16: u16, #[serde(rename = "uint32")] pub uint32: u32, #[serde(rename = "uint8")] pub uint8: u8,}

Code generated from “Enum” schemas

“Enum” schemas will be converted into aRust enum:

{ "enum": ["PENDING", "IN_PROGRESS", "DONE"]}

Generates into:

(Video) Rust & SQL Databases (With Diesel)

use serde::{Deserialize, Serialize};#[derive(Serialize, Deserialize)]pub enum Enum { #[serde(rename = "DONE")] Done, #[serde(rename = "IN_PROGRESS")] InProgress, #[serde(rename = "PENDING")] Pending,}

Code generated from “Elements” schemas

“Elements” schemas will be convertedinto a Rust Vec<T>, where T is the type of the elements of the array:

{ "elements": { "type": "string" }}

Generates into:

pub type Elements = Vec<String>;

Code generated from “Properties” schemas

“Properties” schemas will beconverted into a Rust struct. Optional properties will be wrapped withOptional and a skip_serializing_if on Option::is_none, so they will beomitted from JSON if set to None.

{ "properties": { "name": { "type": "string" }, "isAdmin": { "type": "boolean" } }, "optionalProperties": { "middleName": { "type": "string" } }, "additionalProperties": true}

Generates into:

use serde::{Deserialize, Serialize};#[derive(Serialize, Deserialize)]pub struct Properties { #[serde(rename = "isAdmin")] pub isAdmin: bool, #[serde(rename = "name")] pub name: String, #[serde(rename = "middleName")] #[serde(skip_serializing_if = "Option::is_none")] pub middleName: Option<Box<String>>,}

Code generated from “Values” schemas

“Values” schemas will be converted intoa Rust HashMap<String, T>, where T is the type of the values of the object:

{ "values": { "type": "string" }}

Generates into:

use std::collections::{HashMap};pub type Values = HashMap<String, String>;

Code generated from “Discriminator” schemas

“Discriminator” schemas will beconverted into a Rust enum, and each mapping will be a member of that enum.A set of tags on the discriminator tells serde to use an adjacently taggedrepresentationfor the enum.

(Video) STOP Using Create React App

{ "discriminator": "eventType", "mapping": { "USER_CREATED": { "properties": { "id": { "type": "string" } } }, "USER_PAYMENT_PLAN_CHANGED": { "properties": { "id": { "type": "string" }, "plan": { "enum": ["FREE", "PAID"] } } }, "USER_DELETED": { "properties": { "id": { "type": "string" }, "softDelete": { "type": "boolean" } } } }}

Generates into:

use serde::{Deserialize, Serialize};#[derive(Serialize, Deserialize)]#[serde(tag = "eventType")]pub enum Discriminator { #[serde(rename = "USER_CREATED")] UserCreated(DiscriminatorUserCreated), #[serde(rename = "USER_DELETED")] UserDeleted(DiscriminatorUserDeleted), #[serde(rename = "USER_PAYMENT_PLAN_CHANGED")] UserPaymentPlanChanged(DiscriminatorUserPaymentPlanChanged),}#[derive(Serialize, Deserialize)]pub struct DiscriminatorUserCreated { #[serde(rename = "id")] pub id: String,}#[derive(Serialize, Deserialize)]pub struct DiscriminatorUserDeleted { #[serde(rename = "id")] pub id: String, #[serde(rename = "softDelete")] pub softDelete: bool,}#[derive(Serialize, Deserialize)]pub enum DiscriminatorUserPaymentPlanChangedPlan { #[serde(rename = "FREE")] Free, #[serde(rename = "PAID")] Paid,}#[derive(Serialize, Deserialize)]pub struct DiscriminatorUserPaymentPlanChanged { #[serde(rename = "id")] pub id: String, #[serde(rename = "plan")] pub plan: DiscriminatorUserPaymentPlanChangedPlan,}

Videos

1. you need to create a Cryptocurrency RIGHT NOW!! (Solana token)
(NetworkChuck)
2. Okay but WTF is a MONAD?????? #SoME2
(TheSTEMGamer)
3. [Backend #4] Generate CRUD Golang code from SQL | Compare db/sql, gorm, sqlx & sqlc
(TECH SCHOOL)
4. GraphQL Will Do To REST What JSON Did To XML - Roy Derks
(NDC Conferences)
5. ChatGPT Programming is INSANE!! OpenAI Chat GPT Programming Tutorial and Examples #chatgpt
(Chris Hay)
6. "CodeGen with Types, for Humans, by Humans" by Matthew Griffith (Strange Loop 2022)
(Strange Loop Conference)
Top Articles
Latest Posts
Article information

Author: Allyn Kozey

Last Updated: 03/25/2023

Views: 5753

Rating: 4.2 / 5 (63 voted)

Reviews: 86% of readers found this page helpful

Author information

Name: Allyn Kozey

Birthday: 1993-12-21

Address: Suite 454 40343 Larson Union, Port Melia, TX 16164

Phone: +2456904400762

Job: Investor Administrator

Hobby: Sketching, Puzzles, Pet, Mountaineering, Skydiving, Dowsing, Sports

Introduction: My name is Allyn Kozey, I am a outstanding, colorful, adventurous, encouraging, zealous, tender, helpful person who loves writing and wants to share my knowledge and understanding with you.