Zod
When a Zod schema fails validation, ZodError.issues contains an array of error objects. Each issue has a path property — an array of string and number segments (e.g. ["user", "age"]). Mapping those segments to specific form fields requires comparing them to known field paths. data-path makes that comparison typed and refactor-safe.
Basic mapping
Section titled “Basic mapping”import { z } from "zod";import { path } from "data-path";
const schema = z.object({ user: z.object({ age: z.number().min(18, "Must be at least 18"), email: z.string().email("Invalid email"), }),});
type FormData = z.infer<typeof schema>;
const agePath = path((f: FormData) => f.user.age);const emailPath = path((f: FormData) => f.user.email);
const result = schema.safeParse({ user: { age: 15, email: "bad" } });
if (!result.success) { for (const issue of result.error.issues) { // Compare the Zod segments array directly — no string round-trip. if (agePath.equals({ segments: issue.path })) { console.log("Age error:", issue.message); } if (emailPath.equals({ segments: issue.path })) { console.log("Email error:", issue.message); } }}Building a field error map
Section titled “Building a field error map”A Map keyed by stringified segments avoids the dot-collision hazard of path.join("."):
function buildErrorMap(error: z.ZodError): Map<string, string> { const map = new Map<string, string>(); for (const issue of error.issues) { // JSON.stringify preserves segment boundaries even when a key contains '.' map.set(JSON.stringify(issue.path), issue.message); } return map;}
const errors = buildErrorMap(result.error);
// Look up by typed pathconst ageError = errors.get(JSON.stringify(agePath.segments)); // "Must be at least 18"const emailError = errors.get(JSON.stringify(emailPath.segments)); // "Invalid email"Using agePath.segments as the lookup key keeps the field reference in sync with the schema: if you rename age to birthYear, the path lambda breaks at compile time, and agePath.segments updates automatically.
React Hook Form integration
Section titled “React Hook Form integration”The same pattern applies when using Zod as a resolver with React Hook Form:
import { useForm } from "react-hook-form";import { zodResolver } from "@hookform/resolvers/zod";import { path } from "data-path";
const agePath = path((f: FormData) => f.user.age);
function AgeForm() { const { register, formState: { errors }, } = useForm<FormData>({ resolver: zodResolver(schema) });
// errors object is keyed by field name strings const ageError = agePath.segments.reduce( (obj: Record<string, unknown>, seg) => obj?.[seg] as Record<string, unknown>, errors as Record<string, unknown>, ) as { message?: string } | undefined;
return ( <form> <input {...register(agePath.$)} /> {ageError?.message && <span>{ageError.message}</span>} </form> );}Nested and array paths
Section titled “Nested and array paths”Zod emits numeric indices as real numbers in issue.path (e.g. ["users", 0, "email"]). data-path uses the same string | number segment convention, so the array matches directly — no string conversion required:
const firstUserEmailPath = path((f: FormData) => f.users[0].email);
// Zod issue: { path: ["users", 0, "email"], message: "..." }firstUserEmailPath.equals({ segments: issue.path }); // trueSee also
Section titled “See also”- Data access —
unsafePathfor string-to-path conversion - Relational —
equals,startsWith,match - React Hook Form — typed field registration