I have a vector3d object
const vector3DBase = z
.object({
x: z.number().nullable(),
y: z.number().nullable(),
z: z.number().nullable(),
})
When added to a bigger schema
export const formSchema = z.object({
myPoint: vector3DSchema(),
});
MyPoint can be null from the API. However for validation purposes (and I pass it as lens.focus("myPoint")) I don't want it to be nullable. When I send it to the server I need to transform it and if x,y,z is null then return null, otherwise return object.
However, if I add
.refine(
(val) => {
const allNull = val.x === null && val.y === null && val.z === null;
const allSet = val.x !== null && val.y !== null && val.z !== null;
return allNull || allSet;
},
{
message: "Either all x,y,z must be set, or all must be null",
path: ["x"],
},
)
.transform((val) => {
const allNull = val.x === null && val.y === null && val.z === null;
return allNull ? null : val;
});
zodResolver(formSchema)
Will say that myPoint can be null.
The only way I have found around this is to add .transform on the entire schema but then I have to transform each and every vector3d respectively.
export type FormValues = z.input<typeof formSchema>;
export type ApiValues = z.output<typeof formSchema>;
How can I get zodResolver to use the z.output<typeof formSchema> version of the schema?
Here is where I get a null, when I try to pass a lens to my objects. I have already overridden defaultvalues (from API) so that I guarantee that there are no null values.
<Vector3DComponent
lens={lens.focus("myPoint")}
label={"Adapter axis point"}
/>
Edit: I have added a codesamples. https://stackblitz.com/edit/vitejs-vite-plkpv7pz?file=src%2FMyFormComponent.tsx
You can use this schema to get either a vector with non-nullable coordinates or never instead of your vector object:
const vector3DInput = z
.object({
x: z.number().nullable(),
y: z.number().nullable(),
z: z.number().nullable(),
})
.transform((data, context): Vector3D => {
const { x, y, z: cz } = data;
// Check how many coordinates are null
const nullCount = [x, y, cz].filter((coord) => coord === null).length;
// Return empty vector for
if (nullCount === 3) {
return z.NEVER;
}
// Return filled vector with annotation that it is not null
if (nullCount === 0) {
return { x: x!, y: y!, z: cz! };
}
// Create validation error for partially filled vectors
const nullFields = [];
if (x === null) nullFields.push('x');
if (y === null) nullFields.push('y');
if (cz === null) nullFields.push('z');
context.addIssue({
code: z.ZodIssueCode.custom,
message: `Invalid vector: either all coordinates must be set or all must be null. Missing: ${nullFields.join(
', '
)}`,
});
return z.NEVER;
});
I used a counting-based approach to work around checking every single combination. I also tried constructing a custom error message for easier debugging, should the edge case ever actually happen.
I also adjusted your form component slightly to work with this approach:
export const MyFormComponent = () => {
const { control, handleSubmit } = useForm<FormValues, any, ApiValues>({
resolver: zodResolver(formSchema),
defaultValues: { myPoint: emptyVector3D },
});
const lens = useLens({ control });
const onSubmit = (data: ApiValues) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<SimpleGrid cols={{ base: 1, sm: 1, lg: 2 }} mt={'md'}>
<MyVector3DComponent lens={lens.focus('myPoint')} />
</SimpleGrid>
<Group mt={'sm'} justify={'flex-end'}>
<Button type="submit">Submit</Button>
</Group>
</form>
);
};