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,}
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_json
crate. 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 jtd
crate. What you would do is:
- Parse the input into a
serde_json::Value
, rather than the generated type. - 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. - If the input is valid, then parse the
Value
into your generated type. Youcan do this usingserde_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 GeneratorenumDescription
is likedescription
, but for the members of anenum
. Thekeys ofenumDescription
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:
rustType
overrides the type thatjtd-codegen
should generate.jtd-codegen
will not generate any code for schemas withrustType
, andinstead use the value ofrustType
as-is.It is your responsibility to ensure that the value of
rustType
is validcode.jtd-codegen
will not attempt to validate its value.For example, this schema:
See AlsoCome Combattere l'Ansia da Prestazione Sportiva (Prima, Dopo e Durante la Gara)Morar na Noruega: guia completo para planejar mudança para o paísansi gate valve dimensions - Popular ansi gate valve dimensionsMudpuppy vs Axolotl [How Are These Salamanders Different?]{ "properties": { "name": { "type": "string" }, "isAdmin": { "metadata": { "rustType": "MyCustomType" }, "type": "boolean" } }}
Generates into:
use serde::{Deserialize, Serialize};#[derive(Serialize, Deserialize)]pub struct OverrideDemo { #[serde(rename = "isAdmin")] pub isAdmin: MyCustomType, #[serde(rename = "name")] pub name: String,}
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>
:
{}
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 type | Rust type |
---|---|
boolean | bool |
string | String |
timestamp | chrono::DateTime<chrono::FixedOffset> |
float32 | f32 |
float64 | f64 |
int8 | i8 |
uint8 | u8 |
int16 | i16 |
uint16 | u16 |
int32 | i32 |
uint32 | u32 |
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:
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.
{ "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,}