Schema validation with static type inference
Introduction
Zod is a TypeScript-first validation library.
In web applications, it is common to work with complex data that must be transmitted between different applications.
Using Zod, you can define schemas you can use to validate data, from a simple string to a complex nested object.
Json
When deserializing a JSON string, you can define its structure using a type to provide better autocompletion, error checking, and readability.
Modify the main.ts file:
type Person = name: string,
age: number
}
let str = '{ "name": "Eva", "age": 33}'
let person: Person = JSON.
If you run this code, it works without problems because the deserialized object has the name property:
EvaHowever, this is only informative, as JSON.parse does not verify that the string contains an object compatible with the variable type:
type Person = name: string,
age: number
}
let str = '{"brand": "Seat", "model": "Ibiza"}'
let person: Person = JSON.
If you run the code, the result is undefined because the deserialized object does not have the name property:
undefinedAnd even if in this case the problem is harmless, the same is not true for this code:
type Person = name: string,
age: number
address: Address
}
type Address = street: string,
city: string
}
let str = '{"brand": "Seat", "model": "Ibiza"}'
let person: Person = JSON.
If you run it, you get a serious runtime error:
error: Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'city')
console.log(person.address.city)
^
at file:///Users/david/Workspace/zod/main.ts:16:28Schema
A JSON object does not include any context or metadata, and there is no way to know just by looking at the object what the properties mean or what the allowed values are.
Before you can do anything else, you need to define a schema that contains the description and restrictions that a JSON document must have to be compliant with that schema and, therefore, an instance of that schema.
;
const Player = z. username: z.,
xp: z.
});Given any Zod schema, use .parse to validate an input.
If it’s valid, Zod returns a strongly typed deep clone of the input.
const player = Player.;
When validation fails, the .parse() method will throw a ZodError instance with granular information about the validation issues.
Player.;
} ;
}
}ZodError: [
{
"expected": "string",
"code": "invalid_type",
"path": [
"username"
],
"message": "Invalid input: expected string, received number"
},
{
"expected": "number",
"code": "invalid_type",
"path": [
"xp"
],
"message": "Invalid input: expected number, received string"
}
]
at file:///Users/david/Workspace/zod/main.ts:10:10To avoid a try/catch block, you can use the .safeParse() method to get back a plain result object containing either the successfully parsed data or a ZodError.
The result type is a discriminated union, so you can handle both cases conveniently.
const result = Player.;
result.; // ZodError instance
} result.; // { username: string; xp: number }
}If your schema uses certain asynchronous APIs like async refinements or transforms, you’ll need to use the .safeParseAsync() method instead.
await schema.;Inferring types
Zod infers a static type from your schema definitions.
You can extract this type with the z.infer<> utility and use it however you like.
// extract the inferred type
type Player = z.<typeof Player>;
// use it in your skills
const player: Player = ;
;In some cases, the input & output types of a schema can diverge.
For instance, the .transform() API can convert the input from one type to another.
In these cases, you can extract the input and output types independently:
const mySchema = z..;
type MySchemaIn = z.<typeof mySchema>;
// => string
type MySchemaOut = z.<typeof mySchema>; // equivalent to z.infer<typeof mySchema>
// numberSchema
https://zod.dev/api
To validate data, you must first define a schema.
Schemas represent types, from simple primitive values to complex nested objects and arrays.
Primitives
;
// primitive types
z.;
z.;
z.;
z.;
z.;
z.;
z.;Duck typing
Create a validation schema with two properties, id and name:
;
const schema = z. id: z..,
name: z.,
});You can now use the function you created to validate if an object is compliant with the productSchema schema:
const apple = ;
const result = schema.;
;
} ;
}If you run the code, you can see that we can eat the apple because it is an object that has the properties required for a product:
Let's eat the 🍎And we don’t eat the apple if it doesn’t have the required properties (despite being an apple):
const apple = ;
const result = schema.;
;
} ;
}You already know that TypeScript uses “duck typing.”
Therefore, this object that represents a person, although correct at the code level, is not allowed in real life…
const eva =
const result = schema.;
;
} ;
}I know, I know, many years ago Eva could have been an edible product, but today it is not allowed 🫡
Anyway, this product, although didactic, is not very well designed for what we are using it for.
Modify the product schema and improve the code:
const schema = z. id: z..,
name: z.,
icon: z.,
edible: z.,
});
type Product = z.<typeof schema>;
const json = JSON. '{ "id": 1, "name": "orange", "icon": "🍊", "edible": true }',
);
const result = schema.;
;
} const product = result.;
;
}
}You can see that now we eat the orange 🍊 because it is an edible product.
And… it can still be improved more because TypeScript is “script” with types:
type Product = z.<typeof schema>;Exercises
Below is an object that belongs to a product catalog:
"id": 65,
"name": "A green door",
"price": 12.50,
"tags":
}Create a product schema and infer its type:
Show solution
;
const productSchema = z. id: z..,
name: z.,
price: z.,
tags: z.,
});
// To extract the TypeScript type from the schema:
type Product = z.<typeof productSchema>;
const product: Product = productSchema. "id": 65,
"name": "A green door",
"price": 12.50,
"tags": ,
});
;Fetch
Normally, validation is performed on external data that you get through REST APIs.
It is very important that you validate that data because you can never be sure that the data you receive is correct.
Next, we will use “fake” data from https://jsonplaceholder.typicode.com/.
Modify the index.ts file:
const response = await
const users = await response.
This code downloads a JSON with all the users and shows the names of the users in the terminal:
bun run .\index.ts
[ "Leanne Graham", "Ervin Howell", "Clementine Bauch", "Patricia Lebsack", "Chelsey Dietrich",
"Mrs. Dennis Schulist", "Kurtis Weissnat", "Nicholas Runolfsdottir V", "Glenna Reichert",
"Clementina DuBuque"
]Next, create the typicode.ts file with the corresponding types:
User = id: string
name: string
username: string
email: string
address: Address
phone: string
website: string
company: Company
}
type Address = street: string
suite: string
city: string
zipcode: string
geo: Geo
}
type Geo = lat: number
lng: number
}
type Company = name: string
catchPhrase: string
bs: string
}Use the User type from the typicode.ts script and show the names of the companies where the users work:
from "./typicode"
const response = await
const users: User = await response.
Create the typicode.json file with the validation schema:
> bunx ts-json-schema-generator -p .\typicode.ts -f .\tsconfig.json > .\typicode.jsonValidate the user data:
from "./typicode"
const response = await
const users: User = await response.
const ajv =
const validateUser = ajv.
users.
Activity - DummyJSON
At DummyJSON, you have quite real test data.
1.- At the URL https://dummyjson.com/recipes, you have a list of recipes.
from "./dummyjson"
const response = await
const data = await response.
const recipes: Recipe = data.
Create the dummyjson.ts file with the corresponding types, validation schema, etc.
Show solution
dummyjson.ts
Recipe = id: number
name: string
ingredients: string
instructions: string
prepTimeMinutes: number
cookTimeMinutes: number
servings: number
difficulty: string
cuisine: string
caloriesPerServing: number
tags: string
userId: number
image: string
rating: number
reviewCount: number
mealType: string
}Exercises
https://www.totaltypescript.com/tutorials/zod