H
Hono•5d ago
Mike

Why isn't my dynamically extended Zod schema enforcing types in @hono/zod-openapi?

Hey folks, I'm running into something weird with @hono/zod-openapi I'm dynamically building a schema like this:
export const createChannelSchema = ({ withVideos = false }) => {
let schema = channelSchema;

if (withVideos) {
schema = schema.extend({
videos: z.array(videoSchema),
});
}

return schema.openapi("ChannelResponseSchema");
};
export const createChannelSchema = ({ withVideos = false }) => {
let schema = channelSchema;

if (withVideos) {
schema = schema.extend({
videos: z.array(videoSchema),
});
}

return schema.openapi("ChannelResponseSchema");
};
Then I use this in a route definition like:
import { createRoute } from "@hono/zod-openapi";
const route = createRoute({
...
responses: {
200: jsonContent(createChannelSchema({ withVideos: true }))
}
})
export type GetChannelByIdRoute = typeof route
import { createRoute } from "@hono/zod-openapi";
const route = createRoute({
...
responses: {
200: jsonContent(createChannelSchema({ withVideos: true }))
}
})
export type GetChannelByIdRoute = typeof route
And this is my route handler
import { RouteHandler, RouteConfig } from "@hono/zod-openapi";
type AppRouteHandler<R extends RouteConfig> = RouteHandler<R, AppBindings>;

export const getChannelById: AppRouteHandler<GetChannelByIdRoute> = async (c) => {
const { channelId } = c.req.valid("param");
const channel = await ChannelService.getChannelDetails(channelId);
return c.json(channel, 200);
};
import { RouteHandler, RouteConfig } from "@hono/zod-openapi";
type AppRouteHandler<R extends RouteConfig> = RouteHandler<R, AppBindings>;

export const getChannelById: AppRouteHandler<GetChannelByIdRoute> = async (c) => {
const { channelId } = c.req.valid("param");
const channel = await ChannelService.getChannelDetails(channelId);
return c.json(channel, 200);
};
The issue is: even when channel.videos returned by ChannelService doesn't exist or has the wrong type, no TypeScript error is shown. It only works when I use selectChannelSchema.extends outside of the if statement. Why is that and how do I properly extend my schema dynamically?
13 Replies
ambergristle
ambergristle•5d ago
don't build dynamic schemas
Arjix
Arjix•5d ago
You can add two overloads to the function to fix this issue, when the videos is false, to return a specific type, when it is true, to return another type Right now your function returns a union of those two types (?), you'll have to narrow it down See online how typescript function overloads work, ChatGPT can help you with it as well
Arjix
Arjix•5d ago
GeeksforGeeks
TypeScript Function Overloads - GeeksforGeeks
Your All-in-One Learning Portal: GeeksforGeeks is a comprehensive educational platform that empowers learners across domains-spanning computer science and programming, school education, upskilling, commerce, software tools, competitive exams, and more.
Arjix
Arjix•5d ago
Notice that the overloads are merely types and not implementations, since JavaScript has polymorphism they did it that way
ambergristle
ambergristle•5d ago
an overload will narrow the type if the parameters are known at compile time
Arjix
Arjix•5d ago
In this case they are
ambergristle
ambergristle•5d ago
then why use a factory at all?
Arjix
Arjix•5d ago
Idk man, I am here to solve problems 🤣
ambergristle
ambergristle•5d ago
dynamic zod schemas are an antipattern. factories can be useful in building complex schemas, but it's generally best to compose them statically and use tools like discrimination to deal with variable features
Arjix
Arjix•5d ago
Like
const schemas = {
base: MyZodSchema,
withVideos: MyZodSchema.extend(...)
} as const;

responses: {
200: jsonStuff(schemas.withVideos)
}
const schemas = {
base: MyZodSchema,
withVideos: MyZodSchema.extend(...)
} as const;

responses: {
200: jsonStuff(schemas.withVideos)
}
Oh you meant zod discrimination For payloads? sure For responses? nope
ambergristle
ambergristle•5d ago
why not for responses? you'll need to discriminate client-side to access the resolved type, but you'd need to do that anyway if your endpoint returns a union
Arjix
Arjix•5d ago
The issue is that it doesn't return a union They have multiple routes that return a similar response, the difference being if videos exist or not
ambergristle
ambergristle•5d ago
two static schemas seems like a better solution to that problem, assuming OP meant "factory" instead of "dynamic schema" my point wasn't that discrimination specifically is the right choice for this (or any) use-case, but rather that zod provides a variety of utilities that can be used to build complex schemas statically + without mutation

Did you find this page helpful?