Search code examples
javascripttypescriptzod

How to set the error message in zod refine method?


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?


Solution

  • 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])