I have the following Zod schema:
const createApSchema = z
.object({
name: z.string().min(1).max(32),
isActive: z.boolean().default(true),
description: z.string().max(200).optional(),
ip: z.string().refine(validator.isIP),
accessMode: AccessModeEnum,
apiUsername: z.string().optional(),
apiPassword: z.string().optional(),
apiVersion: ApiVersionEnum,
community: z.string().optional(),
})
.refine((data) => {
// check to see if AP type is snmp to force the community field
const isSnmp = data.accessMode !== AccessModeEnumMap[AccessModeEnum.enum.mikrotikApi]
if (isSnmp && !data.community) throw new Error('community string is required!')
// check to see if AP type is mikrotik to force the api credentials fields
if (!isSnmp && (!data.apiUsername || !data.apiPassword)) throw new Error('api username and password are required!')
if (!isSnmp && !data.apiVersion) throw new Error('api version is required!')
return true
})
And the rule says, if you choose "mikrotikApi" as a value for the accessMode
property, then the fields apiUsername
& apiPassword
& apiVersion
will be required. Otherwise, if choose for example "snmp", then the field community
will be required.
It works, however, in the last block in the chain, in the refine
on the schema object, I am throwing errors in case of a wrong validation. It causes to exit my application. I don't want to exit the app. instead of throwing an error, I want somehow to set the error, so that I can handle it myself. I just want to specify an error message.
How to set the error message in zod refine method?
According the docs on refine
Zod lets you provide custom validation logic via refinements. (For advanced features like creating multiple issues and customizing error codes, see
.superRefine
.)
And the docs on superRefine
show you how to do more complex logic in your refinement. You just call ctx.addIssue
to stack up issues with custom messages.
This might looks something like this:
.superRefine((data, ctx) => {
// check to see if AP type is snmp to force the community field
const isSnmp = data.accessMode !== AccessModeEnumMap[AccessModeEnum.enum.mikrotikApi]
if (isSnmp && !data.community) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'community string is required!'
})
}
// check to see if AP type is mikrotik to force the api credentials fields
if (!isSnmp && (!data.apiUsername || !data.apiPassword)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'api username and password are required!'
})
}
if (!isSnmp && !data.apiVersion) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'api version is required!'
})
}
})
Alternatively, you may not need refine here at all. You actually have a union type here. One member of that union has some required fields that the other member does not require.
This might do the same thing and be much simpler:
const mikroApSchema = z.object({
accessMode: AccessModeEnum.extract(['mikrotikApi']), // only mikrotikApi
apiUsername: z.string(),
apiPassword: z.string(),
apiVersion: ApiVersionEnum,
community: z.string(),
})
const otherApSchema = z.object({
accessMode: AccessModeEnum.exclude(['mikrotikApi']), // everything but mikrotikApi
apiUsername: z.string().optional(),
apiPassword: z.string().optional(),
apiVersion: ApiVersionEnum,
community: z.string().optional(),
})
const createApSchema = z.union([mikroApSchema, otherApSchema])