Follow-Up: Simplifying Zod Validation in Angular Signal Forms with validateStandardSchema
Source: Dev.to
I recently published a tutorial on using Zod validation with Angular Signal Forms, and it worked perfectly.
But a Reddit commenter politely pointed out that I had over‑engineered the entire thing!
In that video I manually wired schema validation, mapped errors, handled success states, and translated field names—only to learn that Angular already ships a built‑in helper specifically designed for schema validators like Zod.
Yes, I completely missed it.
So today we’re fixing that. We’ll delete a lot of code, switch to the right API, and make Zod validation in Angular Signal Forms almost embarrassingly simple.
Stick around – the final solution is shockingly clean.
This post shows the recommended way to use Zod validation in Angular Signal Forms using the built‑in validateStandardSchema() API.
How Zod Validation Works in Angular Signal Forms (Before Refactor)
Let’s start by looking at what the app does right now.
This is a simple signup form that’s already been updated to use the new Signal Forms API:

If I try to submit the form, we immediately get validation errors for both fields:

Those errors come from a Zod schema that’s currently wired into our Signal Form.
Now I’ll enter a valid username and email:

Notice the errors disappear automatically – a good sign.
When I submit the form again, it actually submits the data, which we can confirm with this console log:

So functionally, everything works.
And that’s exactly why this problem is sneaky – the implementation can be much cleaner.
Defining a Zod Validation Schema for Angular Signal Forms
Let’s look at the current code that makes this all work.
We’ll start with the schema file:
import { z } from 'zod';
export const signupSchema = z.object({
username: z
.string()
.min(3, 'Username must be at least 3 characters long')
.regex(/^[a-zA-Z0-9_]+$/, 'Only letters, numbers, and underscores are allowed'),
email: z.string().email('Please enter a valid email address'),
});
This part is excellent – it’s declarative, framework‑agnostic, easy to test, and exactly how schemas should be defined.
The Problem
Scrolling down a bit, we see the custom validateSignup() function that I added in the previous version:
export function validateSignup(value: SignupModel) {
const result = signupSchema.safeParse(value);
if (result.success) {
return {
success: true as const,
data: result.data,
errors: {} as ZodErrorMap,
};
}
const errors = result.error.issues.reduce((acc, issue) => {
const field = issue.path[0]?.toString() ?? '_form';
(acc[field] ??= []).push(issue.message);
return acc;
}, {});
return {
success: false as const,
data: null,
errors,
};
}
This function:
- Calls
safeParse. - Checks whether validation succeeded.
- Reduces Zod issues into a custom error map.
- Reshapes everything into a format Angular understands.
It works, but the file is now doing far more than defining validation rules – that’s our first real issue.
Manual Zod Validation Using validateTree() (Why This Is Overkill)
Signal‑Based Form Model
import { signal } from '@angular/core';
import { SignupModel } from './form.schema';
protected readonly model = signal({
username: '',
email: '',
});
This signal is the single source of truth for the form’s state.
Signal Forms observe this signal and react to changes automatically.
Creating the Form with form()
import { form, validateTree } from '@angular/forms/signals';
import { ValidationError } from '@angular/forms/signals';
import { validateSignup, ZodErrorMap } from './form.schema';
protected readonly form = form(this.model, s => {
validateTree(s, ctx => {
const result = validateSignup(ctx.value());
if (result.success) {
return undefined;
}
const zodErrors: ZodErrorMap = result.errors;
const errors: ValidationError.WithOptionalField[] = [];
const getFieldRef = (key: string) => {
switch (key) {
case 'username':
return ctx.field.username;
case 'email':
return ctx.field.email;
default:
return null;
}
};
for (const [fieldKey, messages] of Object.entries(zodErrors)) {
const fieldRef = getFieldRef(fieldKey);
if (fieldRef) {
errors.push(
...messages.map(message => ({
kind: `zod.${fieldKey}` as const,
message,
field: fieldRef,
}))
);
}
}
return errors.length ? errors : undefined;
});
});
We use validateTree(), Angular’s escape‑hatch validation API, which gives us:
- Full form value access
- Individual field references
- Complete control over validation behavior
While powerful, this approach forces us to write a lot of boilerplate.
The Boilerplate Problem
After the custom validator runs, we handle the success case:
if (result.success) {
return undefined;
}
Then we manually map Zod errors to Angular ValidationError objects:
for (const [fieldKey, messages] of Object.entries(zodErrors)) {
const fieldRef = getFieldRef(fieldKey);
if (fieldRef) {
errors.push(
...messages.map(message => ({
kind: `zod.${fieldKey}` as const,
message,
field: fieldRef,
}))
);
}
}
This busy work doesn’t scale and is exactly what the Reddit commenter called out.
Built‑In Schema Validation: validateStandardSchema()
Angular Signal Forms ships a helper for schema validators like Zod: validateStandardSchema().
It knows how to:
- Run the schema
- Interpret its errors
- Map errors to the correct fields
- Clear errors when values become valid
In short, Angular already knows how to talk to Zod—we just need to let it.
Replacing validateTree() with validateStandardSchema()
Remove all validateTree()‑related code and replace it with:
import { form, validateStandardSchema } from '@angular/forms/signals';
import { signupSchema } from './form.schema';
protected readonly form = form(this.model, s => {
validateStandardSchema(s, signupSchema);
});
- The first argument (
s) is the form scope. - The second argument is the Zod schema (
signupSchema).
That’s it! Angular now runs the schema automatically, maps errors to the correct fields, and clears them as soon as values become valid. No more manual wiring, error translation, or field‑lookup logic.
Simplifying the Schema File
Since the custom validation code is gone, we can delete the validateSignup() function and the ZodErrorMap type from form.schema.ts. The file now only defines the validation rules:
import { z } from 'zod';
export type SignupModel = z.infer<typeof signupSchema>;
export const signupSchema = z.object({
username: z
.string()
.min(3, 'Username must be at least 3 characters long')
.regex(
/^[a-zA-Z0-9_]+$/,
'Only letters, numbers, and underscores are allowed'
),
email: z
.string()
.email('Please enter a valid email address'),
});
The schema file now does one job: define validation rules. Perfect!
Final Result: Clean Zod Validation with Signal Forms
After applying these changes and committing them, the project:
- Uses a single source‑of‑truth signal for form state.
- Leverages Angular’s built‑in
validateStandardSchema()for Zod integration. - Eliminates all manual error‑mapping boilerplate.
- Keeps the schema file focused solely on validation rules.
The codebase is now much cleaner, easier to maintain, and fully takes advantage of Angular Signal Forms’ native capabilities.
Validation Demo
When we submit the form again, the validation errors still appear automatically:
Now let’s enter some valid data:
Perfect. The errors disappear.
When we submit the form again:
Nice! It submits successfully as expected.
So now we have the same behavior with far less code!
When to Use validateStandardSchema() vs validateTree()
Use validateStandardSchema() when:
- You’re using a standard schema validation library (Zod, Yup, Joi, etc.).
- Your schema follows the standard contract (can be parsed, returns errors in a standard format).
- You want the simplest possible integration.
Use validateTree() when:
- You need custom validation logic that doesn’t fit standard schema patterns.
- You’re integrating with a validation library that doesn’t follow standard contracts.
- You need fine‑grained control over error mapping or validation timing.
For most Angular developers using Zod, validateStandardSchema() is the right choice.
This API exists so you don’t have to reinvent schema adapters every time you integrate a validation library.
Best Practice for Zod Validation in Angular Signal Forms
This is one of those Angular APIs that’s easy to miss, but once you see it, you never want to go back.
If you’re using Zod with Signal Forms, don’t manually wire validation like I did. Use validateStandardSchema() instead.
Benefits
- Less code – No manual error mapping or field lookups.
- Fewer bugs – Angular handles the integration correctly.
- Easier to maintain – Schema files stay focused on validation rules.
- Better scalability – Works seamlessly as forms grow in complexity.
- Type safety – Full TypeScript support throughout.
Huge thanks to pkgmain for the catch!
And yes, next time I’ll try to read the docs more carefully before publishing.
Additional Resources
- The demo app BEFORE refactoring (over‑engineered)
- The demo app AFTER refactoring (clean)
- Previous tutorial: Zod Validation with Angular Signal Forms (YouTube)
- Angular Signal Forms documentation
validateStandardSchemaAPI Reference- Zod documentation
- My course Angular: Styling Applications (Pluralsight)
- My course Angular in Practice: Zoneless Change Detection (Pluralsight)


