T
TanStack2mo ago
sunny-green

Validate object field as a single value with Zod

I'm building a form to create an organization in my project. The form includes an address field, which is an object, as shown in the attached schema. The issue I'm facing is with how validation errors are handled. When I submit the form, the errors returned by props.formApi.getAllErrors() put all the address-related errors into form.errors, instead of nesting them under fields.address. Currently, the only way I can validate the address field as an object is by attaching the validator directly to the field itself. However, this approach causes only the address field to be validated on submit (if there is an error on this field), not the entire form. Is there a way, either with Zod or TanStack Form, to validate an object field (like address) directly as a single value — meaning, treating the field as a whole object — instead of validating it through individual nested keys? I'm looking for something like a mode="value" to treat the object as the field's value, and maybe something like mode="object" to allow deep key error handling — similar to how mode="array" works for arrays.
No description
11 Replies
deep-jade
deep-jade2mo ago
are you sure that's the issue? what may be happening is that the fields are mapped too specifically where the address field is not marked as error, but address.streetNumber yes, form.errors will have a copy of the error, but the mapped fields are probably listed in it. Can you share the JSON.stringify version of the form error? Zod v3 -> v4 changed how some paths work, so it can help figure out the problem I have to say, I really like the idea of mode="object" to mark a field as a compound field! I'm tempted to bring that up to other maintainers
sunny-green
sunny-greenOP2mo ago
Here is the only field I have with address in the name. And the JSON of the error
sunny-green
sunny-greenOP2mo ago
A mode object could be cool yes to validate the field as one and not as deep keys for some fields
deep-jade
deep-jade2mo ago
It looks like zod v4's documentation is a bit lacking. This isn't as pretty as I'd like it to be, but it should be worth a try
import z from 'zod/v4';

const addressSchema = z
.object({
address: z.object({
street: z.string().min(1, { error: "Test" }),
}),
})
.check((ctx) => {
ctx.issues = ctx.issues.map((v) => ({ ...v, path: ["address"] }));
});
import z from 'zod/v4';

const addressSchema = z
.object({
address: z.object({
street: z.string().min(1, { error: "Test" }),
}),
})
.check((ctx) => {
ctx.issues = ctx.issues.map((v) => ({ ...v, path: ["address"] }));
});
this one doesn't even check if the issue originates from address, for example
sunny-green
sunny-greenOP2mo ago
This should work yes I think. But I don't know why .check is never trigger. I added a log in check and there is nothing. I tried even with .safeParse and nothing trigger .check
deep-jade
deep-jade2mo ago
zod has a concept of fatal errors vs. non-fatal. For example, if something is undefined when you expect string, it will not perform a min(1) check. Maybe that's why it's not triggered? maybe in this case it's easier to parse first and then edit the return value :Hmm:
validators: {
onChange: ({ value, formApi }) => {
const errors = formApi.parseValuesWithSchema(value) // get the formatted zod errors;
errors.fields // collapse any field that starts with 'address' to 'addess'
// ...
return errors;
}
}
validators: {
onChange: ({ value, formApi }) => {
const errors = formApi.parseValuesWithSchema(value) // get the formatted zod errors;
errors.fields // collapse any field that starts with 'address' to 'addess'
// ...
return errors;
}
}
sunny-green
sunny-greenOP2mo ago
Ok it was the problem for check, my default value was:
address: {
streetNumber: "",
streetName: "",
city: "",
postalCode: "",
country: "",
location: { lat: NaN, lng: NaN },
placeId: "",
}
address: {
streetNumber: "",
streetName: "",
city: "",
postalCode: "",
country: "",
location: { lat: NaN, lng: NaN },
placeId: "",
}
But there it's weird that when lat and lng are NaN there is no error with z.number() but with 1 it works so I tried with changing the path but it's not working. When I changed the pass in the end the path is ['address', 'address']. Is it possible to force fatal errors everywhere ? There is nothing about fatal vs. non-fatal error on zod documentation right ? I can't find it
deep-jade
deep-jade2mo ago
I mostly saw fatal vs. non-fatal when making your own refinements ( superRefine). This probably also applies to check
deep-jade
deep-jade2mo ago
https://zod.dev/api?id=refinements looks like it was replaced by the abort parameter
deep-jade
deep-jade2mo ago
No description
sunny-green
sunny-greenOP2mo ago
Ok I found a way. So first I setup a defaultValue that is valid in term of primitive (So no NaN for a number). Then in the check function I modify all issues with a path of []. Which then convert in the pass of the object. And it seems to work well now.
const organizationAddressSchema = z
.object({
streetNumber: z.string().min(1, 'Le numéro de rue est requis'),
streetName: z.string().min(1, 'Le nom de rue est requis'),
city: z.string().min(1, 'La ville est requise'),
postalCode: z.string().min(1, 'Le code postal est requis'),
country: z.string().min(1, 'Le pays est requis'),
location: z.object(
{
lat: z
.number("L'adresse n'est pas valide")
.min(-90, "L'adresse n'est pas valide")
.max(90, "L'adresse n'est pas valide"),
lng: z
.number("L'adresse n'est pas valide")
.min(-180, "L'adresse n'est pas valide")
.max(180, "L'adresse n'est pas valide"),
},
"L'adresse n'est pas valide"
),
placeId: z.string("L'adresse n'est pas valide"),
})
.check((ctx) => {
ctx.issues = ctx.issues.map((v) => ({
...v,
path: [],
}));
});

{
defaultValues: {
name: "",
description: "",
// email: "",
// phone: "",
siret: "",
slug: "",
logoFile: null as File | null,
address: {
streetNumber: "",
streetName: "",
city: "",
postalCode: "",
country: "",
location: { lat: 1000, lng: 1000 },
placeId: "",
},
}
}
const organizationAddressSchema = z
.object({
streetNumber: z.string().min(1, 'Le numéro de rue est requis'),
streetName: z.string().min(1, 'Le nom de rue est requis'),
city: z.string().min(1, 'La ville est requise'),
postalCode: z.string().min(1, 'Le code postal est requis'),
country: z.string().min(1, 'Le pays est requis'),
location: z.object(
{
lat: z
.number("L'adresse n'est pas valide")
.min(-90, "L'adresse n'est pas valide")
.max(90, "L'adresse n'est pas valide"),
lng: z
.number("L'adresse n'est pas valide")
.min(-180, "L'adresse n'est pas valide")
.max(180, "L'adresse n'est pas valide"),
},
"L'adresse n'est pas valide"
),
placeId: z.string("L'adresse n'est pas valide"),
})
.check((ctx) => {
ctx.issues = ctx.issues.map((v) => ({
...v,
path: [],
}));
});

{
defaultValues: {
name: "",
description: "",
// email: "",
// phone: "",
siret: "",
slug: "",
logoFile: null as File | null,
address: {
streetNumber: "",
streetName: "",
city: "",
postalCode: "",
country: "",
location: { lat: 1000, lng: 1000 },
placeId: "",
},
}
}
Didn't know about this fatal and non-fatal error on zod. Good to know. Maybe could be good to override the continue on the invalid_type of zod primitive or change how the object of zod work but I can't think of a good solution for that to get errors on the parent type

Did you find this page helpful?