fastify-type-provider-zod
fastify-type-provider-zod copied to clipboard
Zod errors messages are weirdly formatted
I noticed validation errors are wierdly formatted, is this just a case of having to parse at the client or is it a bug?
For example: I have the following zod object:
address: z
.string()
.refine((val) => val == "12345", {
message: "This is an error message",
})
This is the error I get, the message is weirdly formatted with new lines:
{
"statusCode": 400,
"error": "Bad Request",
"message": "[\n {\n \"code\": \"custom\",\n \"message\": \"This is an error message\",\n \"path\": [\n \"address\"\n ]\n }\n]"
}
Any ideas?
It is fastify that stringifies the error, but there is a way to make it cleaner. I will take a look at that.
Was looking to see if that can be disabled, but couldn't find a way to hook into it at the code level.
@eh-93 You can implement custom errorHandler on fastify level to format your errors
I meant at the library level. The logic to determine whether the message needs formatting or not would get really messy and probably slow down the req/res cycle. I think this is an issue of consistency as opposed to custom error handling.
Not all error messages are stringified, for example if you return an error in the handler:
reply.status(404).send({
error: "Not found",
message: "Not Found",
});
The message in the response body isn't stringified and is missing the status code that would be present in the case of the error being thrown at the zod validation stage:
{
"error": "Not found",
"message": "Not Found"
}
I've added a custom errorHandler like so:
app.setErrorHandler((error, request, reply) => {
if (error instanceof ZodError) {
console.log("Error me out", JSON.parse(error.message).message);
reply.status(400).send({
message: "Validation error",
errors: JSON.parse(error.message),
});
}
});
That works fine functionally, but not ideal because that error schema never makes it to swagger
We can remove the newlines(\n
) from the stringified message but looks like you want the message to be a JSON object, I don't think we can interfere it at validation time.
I was just tinkering, Stripping the new lines so that it's just a message would help.
I've now hooked into the error handler and have managed to do that at the fastify level similar to the example above. But the downside of that is that the error schema isn't reflected in the generated OpenAPI doc.
to followup on @eh-93 workaround, you do not need to do the JSON.parse, the parsed error is already there
app.setErrorHandler((error, request, reply) => {
if (error instanceof ZodError) {
reply.status(400).send({
statusCode: 400,
error: "Bad Request",
issues: error.issues,
});
return;
}
reply.send(error);
})
Don't forget do return
server.setErrorHandler((error, request, reply) => {
if (error instanceof ZodError) {
reply.status(400).send({
statusCode: 400,
error: 'Bad Request',
message: 'Validation error',
errors: JSON.parse(error.message),
});
return;
}
reply.send(error);
});
Thank you @elie-h and @Hurtak !
One additional thing that still frustrated me about these error responses is that they don't tell you which part of the request failed the schema validation; querystring, body, headers, params?
I found that by customizing the validator compiler a bit you can pass it down to the error handler.
import {type FastifyInstance} from 'fastify'
import {serializerCompiler, validatorCompiler} from 'fastify-type-provider-zod'
import {FastifyRouteSchemaDef} from 'fastify/types/schema'
import {ZodAny, ZodError} from 'zod'
async function registerZod(fastify: FastifyInstance) {
fastify.setValidatorCompiler((req: FastifyRouteSchemaDef<ZodAny>) => {
return (data: any) => {
const res = validatorCompiler(req)(data)
// @ts-ignore
if (res.error) res.error.httpPart = req.httpPart
return res
}
})
fastify.setSerializerCompiler(serializerCompiler)
// The default zod error serialization is awful
// See: https://github.com/turkerdev/fastify-type-provider-zod/issues/26#issuecomment-1322254607
fastify.setErrorHandler((error, request, reply) => {
if (error instanceof ZodError) {
reply.status(400).send({
statusCode: 400,
error: 'Bad Request',
// @ts-ignore -- This is set in the validator compiler above 👆
httpPart: error.httpPart,
issues: error.issues,
})
return
}
reply.send(error)
})
}
An improvement with TypeScript integration.
// src/config/config.type.ts
import "zod"
declare module "zod" {
interface ZodError {
httpPart?: string
}
}
// src/config/config.service.ts
function registerZod(server: FastifyInstance) {
server.setValidatorCompiler((req: FastifyRouteSchemaDef<ZodAny>) => {
return async (data: unknown) => {
const res = await validatorCompiler(req)(data)
if (res.error) res.error.httpPart = req.httpPart
return res
}
})
server.setSerializerCompiler(serializerCompiler)
server.setErrorHandler(
(error: FastifyError, _: FastifyRequest, reply: FastifyReply) => {
if (error instanceof ZodError)
reply.status(status.BAD_REQUEST).send({
statusCode: status.BAD_REQUEST,
error: "Bad Request",
httpPart: error.httpPart,
issues: error.issues,
})
reply.send(error)
},
)
}