Zod
Demo App
Basic Usage
Creating a simple string schema
from anvil_labs import zod as z
# create a schema
schema = z.string()
# parsing
schema.parse("tuna") # -> "tuna"
schema.parse(42) # -> throws ParseError
# "safe" parsing - doesn't throw if valid
result = schema.safe_parse("tuna") # -> ParseResult(success=True, data="tuna")
result.success # True
result = schema.safe_parse(42) # -> ParseResult(success=False, error=ParseError("Invalid type"))
result.success # False
Creating a typed_dict schema
from anvil_labs import zod as z
# create a schema
user = z.typed_dict({
"username": z.string()
})
user.parse({"username": "Meredydd"}) # -> {"username": "Meredydd"}
Primitives
from anvil_labs import zod as z
z.string()
z.integer()
z.float()
z.number() # int or float
z.boolean()
z.date()
z.datetime()
z.none()
# catch all types - allow any value
z.any()
z.unknown()
# never types - allows no values
z.never()
Literals
from anvil_labs import zod as z
tuna = z.literal("tuna")
empty_str = z.literal("")
true = z.literal(True)
_42 = z.literal(42)
# retrieve the literal value
tuna.value # "tuna"
Strings
Zod includes a handful of string-specific validations.
z.string().max(5)
z.string().min(5)
z.string().len(5)
z.string().email()
z.string().url()
z.string().uuid()
z.string().regex(re.compile(r"^\d+$""))
z.string().startswith(string)
z.string().endswith(string)
z.string().strip() # strips whitespace
z.string().datetime() # defaults to iso format string
z.string().date() # defaults to iso format string
You can customize some common error messages when creating a string schema.
name = z.string(
required_error="Name is required",
invalid_type_error="Name must be a string",
)
When using validation methods, you can pass in an additional argument to provide a custom error message
z.string().min(5, message="Must be 5 or more characters long")
z.string().max(5, message="Must be 5 or fewer characters long")
z.string().length(5, message="Must be exactly 5 characters long")
z.string().email(message="Invalid email address")
z.string().url(message="Invalid url")
z.string().uuid(message="Invalid UUID")
z.string().startswith("https://", message="Must provide secure URL")
z.string().endswith(".com", message="Only .com domains allowed")
z.string().datetime(message="Invalid datetime string! Must be in isoformat")
Coercion for primitives
Zod provides a convenient way to coerce primitive values.
schema = z.coerce.string()
# remove print statements
schema.parse("tuna") # => "tuna"
schema.parse(12) # => "12"
schema.parse(True) # => "True"
During the parsing step, the input is passed through the str()
function.
Note that the returned schema is a ZodString instance so you can use all string methods.
z.coerce.string().email().min(5)
The following primitive types support coercion
z.coerce.string() # str(input)
z.coerce.boolean() # bool(input)
z.coerce.integer() # int(input)
z.coerce.float() # float(input)
The int and float coercions will be surrounded in a try/except. This way coercion failures will be reported as invalid type errors.
Numbers, Integers and Floats
Zod integer and float expect their equivalent python types when parsed. A zod number accepts either integer or float.
from anvil_labs.zod import z
age = z.number(
required_error="Age is required",
invalid_type_error="Age must be a number",
)
Zod includes a handful of number-specific validations.
from anvil_labs.zod import z
z.number().gt(5)
z.number().ge(5) # greater than or equal to, alias .min(5)
z.number().lt(5)
z.number().le(5) # less than or equal to, alias .max(5)
z.number().int() # value must be an integer
z.number().positive() # > 0
z.number().nonnegative() # >= 0
z.number().negative() # < 0
z.number().nonpositive() # <= 0
The equivalent validations are available on integer
and float
.
Optionally, you can pass in a second argument to provide a custom error message.
z.number().le(5, message="this👏is👏too👏big")
Booleans
You can customize certain error messages when creating a boolean schema
is_active = z.boolean(
required_error="isActive is required",
invalid_type_error="isActive must be a boolean",
)
Dates and Datetimes
from anvil_labs.zod import z
from datetime import date
z.date().safe_parse(date.today()) # success: True
z.date().safe_parse("2022-01-12") # success: False
You can customize the error messages
my_date_schema = z.date(
required_error="Please select a date and time",
invalid_type_error="That's not a date!",
)
Zod provides a handful of datetime-specific validations.
z.date().min(
date(1900, 1, 1),
message="Too old"
)
z.date().max(
date.today(),
message="Too young!"
)
Supporting date strings
def preprocess_date(arg):
if isinstance(arg, str):
try:
return date.fromisoformat(arg) #could use datetime.strptime().date
except Exception:
return arg
else:
return arg
date_schema = z.preprocess(preprocess_date, z.date())
date_schema.safe_parse(date(2022, 1, 12)) # success: True
date_schema.safe_parse("2022-01-12") # success: True
Enums
from anvil_labs.zod import z
FishEnum = z.enum(["Salmon", "Tuna", "Trout"])
z.enum
is a way to declare a schema with a fixed set of allowable values.
Pass the list of values directly into z.enum()
.
To retrieve the enum options use .options
FishEnum.options # ["Salmon", "Tuna", "Trout
Optional
Optional is synonymous with python’s typing.Optional.
In other words, something optional can also be None.
(This is different to Zod TypeScript’s optional
)
from anvil_labs.zod import z
schema = z.optional(z.string())
schema.parse(None) # returns None
For convenience, you can also call the .optional()
method on an existing schema.
schema = z.string().optional()
You can extract the wrapped schema from a ZodOptional instance with .unwrap()
.
string_schema = z.string()
optional_string = string_schema.optional()
optional_string.unwrap() == string_schema # True
TypedDict
This is equivalent to Zod TypeScript’s object
schema.
We chose typed_dict
since it matches Python’s typing.TypedDict
.
(z.object
is also available for convenience)
from anvil_labs.zod import z
# all properties are required by default
Dog = z.typed_dict({
"name": z.string(),
"age": z.number()
})
API
- class ZodTypedDict
- shape
Use
.shape
to access the schemas for a particular key.Dog.shape["name"] # => string schema Dog.shape["age"] # => number schema
- keyof()
Use
.keyof
to create a ZodEnum schema from the keys of a typed_dict schema.key_schema = Dog.keyof() key_schema # ZodEnum<["name", "age"]>
- extend()
You can add additional fields to a typed_dict schema with the .extend method.
from anvil_labs.zod import z # all properties are required by default Dog = z.typed_dict({ "name": z.string(), "age": z.number() }) DogWithBreed = Dog.extend({ "breed": z.string() })
You can use
.extend
to overwrite fields! Be careful with this power!
- merge(B)
Equivalent to
A.extend(B.shape)
.If the two schemas share keys, the properties of B overrides the property of A. The returned schema also inherits the “unknownKeys” policy (strip/strict/passthrough) and the catchall schema of B.
BaseTeacher = z.typed_dict({ "students": z.list(z.string()) }) HasID = z.typed_dict({ "id": z.string() }) Teacher = BaseTeacher.merge(HasID) # the type of the `Teacher` variable is inferred as follows: # { # "students": z.array(z.string()), # "id": z.string() # }
- pick(keys=None)
Returns a modified version of the typed_dict schema that only includes the keys specified in the
keys
argument. (This method is inspired by TypeScript’s built-inPick
utility type).from anvil_labs.zod import z Recipe = z.typed_dict({ "id": z.string(), "name": z.string(), "ingredients": z.list(z.string()), }) JustTheName = Recipe.pick(["name"]) # the type of the JustTheName variable is inferred as follows: # { # "name": z.string() # }
- omit(keys=None)
Returns a modified version of the typed_dict schema that excludes the keys specified in the
keys
argument. (This method is inspired by TypeScript’s built-inOmit
utility type).from anvil_labs.zod import z Recipe = z.typed_dict({ "id": z.string(), "name": z.string(), "ingredients": z.list(z.string()), }) NoIDRecipe = Recipe.omit(["id"]) # the type of the `NoIDRecipe` variable is inferred as follows: # { # "name": z.string(), # "ingredients": z.list(z.string()) # }
- partial(keys=None)
- Returns:
a modified version of the typed_dict schema in which all properties are made optional. This method is inspired by the built-in TypeScript utility type Partial.
- Parameters:
keys (iterable) – Optional argument that specifies which properties to make optional. If not provided, all properties are made optional.
from anvil_labs.zod import z User = z.typed_dict({ "email": z.string(), "username": z.string(), }) # create a partial version of the `User` schema PartialUser = User.partial() PartialUser.parse({"email": "foo@gmail.com"}) # -> {"email": "foo@gmail.com"} PartialUser.parse({}) # -> {} PartialUser.parse({"email": None}) # -> raises ParseError
the type of the PartialUser variable is equivalent to:
{ "email": z.string().not_required(), "username": z.string().not_required(), }
In other words the parsed dictionary may or may not include the
"email"
and"username"
key. Note this is different to.optional()
which would allow the value to be NoneCreate a partial version of the User schema where only the email property is made optional
OptionalEmail = User.partial(["email"]) # the type of the `OptionalEmail` variable is equivalent to: # { # "email": z.string().not_required(), # "username": z.string(), # }
- required(keys=None)
Returns a modified version of the typed_dict schema in which all properties are made required. This method is the opposite of the
.partial
method, which makes all properties optional.- Parameters:
keys (iterable) – Optional argument that specifies which properties to make required. If not provided, all properties are made required.
from anvil_labs.zod import z User = z.typed_dict({ "email": z.string(), "username": z.string(), }).partial() # create a required version of the `User` schema RequiredUser = User.required()
RequiredUser
is now equivalent to the original shape.Create a required version of the
User
schema where only theemail
property is made requiredRequiredEmail = User.required(["email"]) # the type of the `RequiredEmail` variable is equivalent to: # { # "email": z.string(), # "username": z.string().not_required(), # }
- passthrough()
Returns a modified version of the typed_dict schema that enables
"passthrough"
mode. In passthrough mode, unrecognized keys are not stripped out during parsing.from anvil_labs.zod import z Person = z.typed_dict({ "name": z.string(), }) # parse a dict with unrecognized keys result = Person.parse({ "name": "bob dylan", "extraKey": 61, }) # the `result` variable is as follows: # { # "name": "bob dylan", # }
The
extraKey
property has been stripped out because thePerson
schema is not in"passthrough"
mode# enable "passthrough" mode for the `Person` schema PassthroughPerson = Person.passthrough() # parse a dict with unrecognized keys result = PassthroughPerson.parse({ "name": "bob dylan", "extraKey": 61, }) # the `result` variable is now as follows: # { # "name": "bob dylan", # "extraKey": 61, # }
Now the
extraKey
property has not been stripped out because thePassthroughPerson
schema is in"passthrough"
mode
- strict()
Returns a modified version of the typed_dict schema that disallows unknown keys during parsing. If the input to
.parse()
contains any unknown keys, aParseError
will be thrown.from anvil_labs.zod import z Person = z.typed_dict({ "name": z.string(), }) # parse a dict with unrecognized keys try: result = Person.strict().parse({ "name": "bob dylan", "extraKey": 61, }) except z.ParseError as e: print(e) # => "Unknown key 'extraKey' found in input at path 'extraKey'"
The code above will throw a ParseError because the
Person
schema is in"strict"
mode and the input contains an unknown key
- strip()
Returns a modified version of the typed_dict schema that strips out unrecognized keys during parsing. This is the default behavior of ZodTypedDict schemas.
- catchall(schema: ZodAny) ZodTypedDict
You can pass a
"catchall"
schema into a typed_dict schema. All unknown keys will be validated against it.- Parameters:
schema – A Zod schema for validating unknown keys.
- Returns:
A new ZodTypedDict schema with catchall schema for unknown keys.
- Raises:
ParseError – If any unknown keys fail validation.
Example:
from zod import z # Create a person schema with `name` field person = z.typed_dict({ "name": z.string() }) # Add a catchall schema for any unknown keys person = person.catchall(z.number()) # Parse with valid extra key person.parse({ "name": "bob dylan", "validExtraKey": 61 }) # Parse with invalid extra key person.parse({ "name": "bob dylan", "invalidExtraKey": "foo" }) # => raises ParseError
Using
.catchall()
obviates.passthrough()
,.strip()
, or.strict()
. All keys are now considered “known”.
NotRequired
The .not_required()
method can be used in conjunction with typed_dict schemas.
This means the key value pair can be missing. See the ZodTypedDict.partial()
method.
List
Similar to typing.List
type.
string_list = z.list(z.string())
# equivalent
string_array = z.string().list()
Be careful with the .list()
method.
It returns a new ZodList instance.
This means the order in which you call methods matters. For instance:
z.string().optional().list() # (string | None)[]
z.string().list().optional() # string[] | None
A ZodList schema will parse a tuple
or list
.
A tuple
will be returned as a list
upon parsing.
The following method are provided on a list
schema
z.string().list().min(5) # must contain 5 or more items
z.string().list().max(5) # must contain 5 or fewer items
z.string().list().len(5) # must contain 5 items exactly
Additional API
- class ZodList
- element
Use
.element
to access the schema for an element of the array.string_list.element; # => string schema
- nonempty(message)
If you want to ensure that an array contains at least one element, use
.nonempty()
.- Parameters:
message – Optional custom error message.
- Returns:
The same ZodList instance with
.nonempty()
added.
Example:
non_empty_strings = z.string().list().nonempty(); non_empty_strings.parse([]); // throws: "List cannot be empty" non_empty_strings.parse(["Ariana Grande"]); # passes
You can optionally specify a custom error message:
from anvil_labs import zod as z # optional custom error message non_empty_strings = z.string().array().nonempty( message="Can't be empty!" )
Tuples
Unlike lists, tuples have a fixed number of elements and each element can have a different type.
It is similar to typing.Tuple
type.
athlete_schema = z.tuple([
z.string(), # name
z.integer(), # jersey number
z.dict({"points_scored": z.number()}) # statistics
])
A variadic (“rest”) argument can be added with the .rest
method.
from anvil_labs import zod as z
variadic_tuple = z.tuple([z.string()]).rest(z.number())
result = variadic_tuple.parse(["hello", 1, 2, 3])]
For convenience a tuple schema will parse both A list
and a tuple
in the same way.
Unions
Zod includes a built-in z.union
method for composing “OR” types.
This is similar to typing.Union
.
string_or_number = z.union([z.string(), z.number()])
string_or_number.parse("foo") # passes
string_or_number.parse(14) # passes
Zod will test the input against each of the “options” in order and return the first value that validates successfully.
For convenience, you can also use the .union
method:
string_or_number = z.string().union(z.number())
Mappings
Mappings are similar to Python’s typing.Mapping
or typing.Dict
types.
You should specify a key and value schema
NumberCache = z.mapping(z.string(), z.integer());
# expects to parse dict[str, int]
This is particularly useful for storing or caching items by ID
user_schema = z.typed_dict({"name": z.string()})
user_cache_schema = z.mapping(z.string().uuid(), user_schema)
user_store = {}
user_store["77d2586b-9e8e-4ecf-8b21-ea7e0530eadd"] = {"name": "Carlotta"}
user_cache_schema.parse(user_store) # passes
user_store["77d2586b-9e8e-4ecf-8b21-ea7e0530eadd"] = {"whatever": "Ice cream sundae"}
user_cache_schema.parse(user_store) # Fails
Recursive types
from anvil_labs import zod as z
Category = z.lazy(lambda:
z.typed_dict({
'name': z.string(),
'subcategories': z.list(Category),
})
)
Category.parse({
'name': 'People',
'subcategories': [
{
'name': 'Politicians',
'subcategories': [{ 'name': 'Presidents', 'subcategories': [] }],
},
],
}) # passes
If you want to validate any JSON value, you can use the snippet below.
literal_schema = z.union([z.string(), z.number(), z.boolean(), z.none()])
json_schema = z.lazy(lambda: z.union([literal_schema, z.list(json_schema), z.mapping(json_schema)]))
json_schema.parse(data)
Isinstance
You can use z.isinstance
to check that the input is an instance of a class.
This is useful to validate inputs against classes.
from anvil_labs import zod as z
class Test:
def __init__(self, name: str):
self.name = name
TestSchema = z.isinstance(Test)
blob = "whatever"
TestSchema.parse(Test("my_name")) # passes
TestSchema.parse(blob) # throws
Preprocess
Typically Zod operates under a “parse then transform” paradigm. Zod validates the input first, then passes it through a chain of transformation functions. (For more information about transforms)
But sometimes you want to apply some transform to the input before parsing happens. A common use case: type coercion.
Zod enables this with the z.preprocess()
.
cast_to_string = z.preprocess(lambda val: str(val), z.string())
Schema Methods
- parse(data)
- Returns:
If the given value is valid according to the schema, a value is returned. Otherwise, an error is thrown.
IMPORTANT: The value returned by .parse is a deep clone of the variable you passed in.
- Example:
string_schema = z.string() string_schema.parse("fish") # returns "fish" string_schema.parse(12) # throws ParseError
- safe_parse(data)
- Returns:
ParseResult(success: bool, data: any, error: ParseError | None)
If you don’t want Zod to throw errors when validation fails, use
.safe_parse
. This method returns a ParseResult containing either the successfully parsed data or a ParseError instance containing detailed information about the validation problems.- Example:
string_schema.safe_parse(12) # ParseResult(success=False, error=ParseError) string_schema.safe_parse("fish") # ParseResult(success=True, data="fish")
You can handle the errors conveniently:
result = stringSchema.safeParse("billie") if not result.success: # handle error then return print(result.error) else: # do something print(result.data)
Not Yet Documented:
refine
super_refine
transform
super_transform
default
catch
optional
error handling and formatting