Awesome
zod-to-nestjs-graphql
A package to generate GraphQL types from zod
contracts.
Inspired by nestjs-graphql-zod
package. Thanks to Tarık İnce!
How to use
Package exports three functions:
generateObjectTypeFromZod
– takes azod
contract and returns Object type.generateInputTypeFromZod
– takes azod
contract and returns Input type.registerZodEnumType
– registers enumeration declared usingzod
(bothz.enum()
andz.nativeEnum()
).
Important notes
Naming
The GraphQL schema became very powerful when types it consist have comprehensive and understandable names. To achieve that, we need a fully manageable way to deal with types naming. And this package insists that way – every single operation requires you to provide explicit name for type.
Declaration order
The order in which functions are called is important. You must start from the most nested type/enum and go through the least ones.
Types support and mapping
z.date()
type maps toGraphQLISODateTime
(from@nestjs/graphql
).z.string().uuid()
type maps toGraphQLUUID
(fromgraphql-scalars
).z.number().int()
type maps toInt
(from@nestjs/graphql
).z.any()
andz.unknown()
types maps toGraphQLJSON
(fromgraphql-scalars
).z.record()
type (with all the arguments) maps toGraphQLJSONObject
(fromgraphql-scalars
).
Basic usage
Imagine that you have the following contracts:
// contracts.ts
import { z } from 'zod'
export const AuthType = z.nativeEnum({
EMAIL: 'EMAIL',
PHONE: 'PHONE',
} as const)
export const CountryEntity = z.object({
code: z.string(),
name: z.string(),
})
export const UserEntity = z.object({
id: z.string().uuid(),
name: z.string().describe('User name.'),
age: z.number().int().describe('User age.'),
authType: AuthType,
country: CountryContract,
dataBin: z.any(),
someAmorphousData: z.record(z.string(), z.unknown())
})
To transform them up to GraphQL types suitable for NestJS, you need to:
// auth.types.ts
import { AuthType, CountryEntity, UserEntity } from 'contracts.ts'
import { registerZodEnumType, generateObjectTypeFromZod } from 'zod-to-nestjs-graphql'
registerZodEnumType(AuthType, {
name: 'AuthType',
});
export const User = generateObjectTypeFromZod(UserEntity, {
name: 'User',
});
...then in your resolvers:
// auth.resolver.ts
import { User } from 'auth.types.ts'
@Resolver()
export class AuthResolver {
@Query(() => User)
me() {
// ...
}
}
And it will produce the following GraphQL schema:
enum AuthType {
PHONE
EMAIL
}
type Country {
code: String!
name: String!
}
type User {
id: UUID!
"""User name."""
name: String!
"""User age."""
age: Int!
authType: AuthType!
country: Country!
dataBin: GraphQLJSON!
someAmorphousData: GraphQLJSONObject!
}
You don't need to worry about nested objects (like CountryEntity
in UserEntity
)
because they're registered automatically (with auto-generated names).
GraphQL types auto-registration and name generation
Types auto-registration is enabled by default.
Let's take a case study based on the example above.
// auth.types.ts
// ...
export const User = generateObjectTypeFromZod(UserEntity, {
name: 'User',
});
generateObjectTypeFromZod
here registers two objects.
One is UserEntity
which is transformed to GraphQL Object type with name User
.
Its name is explicitly supplied in the name
option.
Another one is CountryEntity
.
It nested to UserEntity
object, in country
key.
Corresponding GraphQL Object type will be named Country
–
the name is generated based on the key name, first transformed to PascalCase
(in according to GraphQL naming recommendations).
A few more examples:
export const Person = z.object({
country: CountryContract, // Country
personalDetails: DetailsContract, // PersonalDetails
misc_data: MiscContract // MiscData
})
Advanced usage
Overriding auto-generated names of types
From the example above:
// auth.types.ts
// ...
export const User = generateObjectTypeFromZod(UserEntity, {
name: 'User'
});
To override auto-generated name, register an object manually, before it happens automatically (before you will call generateObjectTypeFromZod
):
// auth.types.ts
// Preliminary registering nested type to take full control of it.
export const Country = generateObjectTypeFromZod(CountryEntity, {
name: 'MySpecialNameOfCountry',
description: 'Country object.'
});
export const User = generateObjectTypeFromZod(UserEntity, {
name: 'User'
});
Or, in cases if you don't use a newly registered GraphQL type anywhere in resolvers, there is one more way to do this:
// auth.types.ts
// ...
export const User = generateObjectTypeFromZod(
UserEntity,
{ name: 'User' },
{
// The order of registration is still important. Register most nested types first.
additionalRegistrations: [
[
Country,
{
name: 'MySpecialNameOfCountry',
description: 'Country object.'
}
],
],
}
);
By this we get the same result as in the example above.
Hot replacements
Occasionally you will need to change the type on the fly. Let's take contracts with highly nested objects inside:
// contracts.ts
import { z } from 'zod'
export const NestedDataContract = z.object({
data: z.object({}),
})
// A contract with highly nesting.
export const UserDataContract = z.object({
// Property that have many nested objects inside.
data: NestedDataContract,
})
export const UserEntity = z.object({
id: z.string().uuid(),
name: z.string().describe('User name.'),
data: UserDataContract,
})
In case when you don't want to register all these nested types to GraphQL too, just replace it with more appropriate:
// auth.types.ts
import { AuthType, CountryEntity, UserEntity } from 'contracts.ts'
import { registerZodEnumType, generateObjectTypeFromZod } from 'zod-to-nestjs-graphql'
registerZodEnumType(AuthType, {
name: 'AuthType',
});
export const User = generateObjectTypeFromZod(
UserEntity,
{ name: 'User' },
{
hotReplacements: [
{
// Replace value of “data” in “UserDataContract” (“NestedDataContract”)
origin: User.shape.data.shape.data,
// ...with just “GraphQLJSONObject”.
replacement: z.record(z.string(), z.unknown())
},
],
}
);
After this, the GraphQL schema would look like:
type User {
id: UUID!
"""User name."""
name: String!
data: GraphQLJSONObject!
}