Zod 3 had several ways to customize messages: a message string on most methods, plus invalid_type_error and required_error on schema constructors, plus errorMap for full control. Zod 4 collapses these into one error parameter that accepts either a string or a function receiving the issue.
invalid_type_error and required_error were removed, so code passing them to z.string({ required_error: 'Name is required' }) silently gets no custom message, because the unknown key is ignored rather than rejected. That is the failure worth looking for during a migration: messages revert to defaults with no error anywhere.
The replacement is a function that inspects the issue: z.string({ error: (iss) => iss.input === undefined ? 'Name is required' : 'Must be a string' }). The message key is still accepted but deprecated. errorMap was likewise folded into the same error parameter at the parse level, so a global customization is now passed as error in the parse options.