Download the PHP package dragosstoenica/laravel-zod without Composer
On this page you can find all versions of the php package dragosstoenica/laravel-zod. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download dragosstoenica/laravel-zod
More information about dragosstoenica/laravel-zod
Files in dragosstoenica/laravel-zod
Package laravel-zod
Short Description Generate Zod v4 TypeScript schemas from Spatie Laravel Data classes (output) and Laravel FormRequest classes (input). Full Laravel rule coverage, schema references for nested Data classes, lazy-ref dedup for circular relations, locale-aware messages.
License MIT
Informations about the package laravel-zod
dragosstoenica/laravel-zod
Generate Zod 4 schemas from Laravel FormRequest classes (input) and Spatie laravel-data DTOs (output). PHP stays the source of truth; TypeScript gets runtime parsing and inferred types from a single file.
Compatibility: PHP 8.3 / 8.4 / 8.5 · Laravel 11 / 12 / 13 · Zod 4.x · Spatie Data 4.22+
Why this exists
Other generators have these problems:
| Pattern | Other generators | This package |
|---|---|---|
?UserData $host on a Data class |
inlined z.object({...}) (no schema reuse) |
host: UserDataSchema.nullable() ✓ |
EventAttendeeData[] array |
inlined repeatedly | z.array(EventAttendeeDataSchema) ✓ |
| Required string | z.string({error}).trim().refine(...).min(1) (belt-and-suspenders) |
z.string().trim().min(1, '...') ✓ |
| Output schema | dragged form-error messages | type narrowing + nullability only ✓ |
| Schema declaration order | filenames or random | topologically sorted ✓ |
| Circular refs (self / mutual) | TDZ error at runtime | z.lazy(() => Schema) on back-edges ✓ |
Cross-field rules (after:other, required_if, …) |
partial / inline | one .superRefine() block at schema bottom ✓ |
| Locale | hardcoded English | --locale=ro + Laravel lang file fallback chain ✓ |
Install
The package's service provider auto-registers under Laravel's package discovery — no manual provider entry needed.
If you're consuming via a Composer path repository (recommended while iterating):
Usage
1. Mark classes with #[ZodSchema]
2. Generate
3. Consume from TypeScript
The generated file exports *Schema constants and *SchemaType aliases (the latter is a z.infer<> for convenience).
Configuration
config/laravel-zod.php:
Locales / messages
Resolution order, per (field, rule):
FormRequest::messages()exact key —'title.required' => 'Pick a name.'- Wildcard —
'*.required' => ':attribute is mandatory.' - Locale validation file —
lang/<locale>/validation.php(validation.required) - English fallback —
lang/en/validation.php - Humanised fallback —
Headline-cased validation failed.
Placeholders filled: :attribute (from attributes() or the field name), :min, :max, :size, :other, :value, :date, :format, :digits, :decimal, :values.
For sub-keyed rules (min/max/between/size/gt/gte/lt/lte), the package picks the correct sub-key based on the field's inferred type:
To add a non-English locale:
Custom Rule classes
Any value in rules() can be a Rule object. Two ways the package handles them:
Opt-in: implement HasZodSchema
Stringifiable rules (e.g. Rule::in([...]), Rule::enum(MyEnum::class))
The package recognises Laravel's built-in stringifiable Rule objects and re-runs them through the rule translator.
Everything else
A custom Rule object that implements neither HasZodSchema nor a recognised __toString is skipped with a warning and a // custom rule skipped: … comment in the schema. Set 'custom_rules_strict' => true in the config to fail-loud instead.
Circular dependencies
Self-references (Comment.parent: ?Comment) and mutual references (Author.posts: Post[] ↔ Post.author: Author) are detected during topological sort. The back-edge gets wrapped in z.lazy(() => Schema) automatically:
You don't need to do anything in PHP — annotate the relations as plain nullable types (?UserData, ?array with @var X[]) and let the generator pick the right side to defer.
Rule coverage
All rules are translated to client-side Zod where the semantics map. Cross-field rules become .superRefine() blocks. DB-backed rules emit a // server-only comment.
| Family | Rules |
|---|---|
| Presence | required, nullable, sometimes, present, filled, bail |
| Conditional presence | required_if, required_unless, required_if_accepted, required_if_declined, required_with, required_with_all, required_without, required_without_all, required_array_keys |
| Missing / prohibited | missing, missing_if, missing_unless, missing_with, missing_with_all, prohibited, prohibited_if, prohibited_if_accepted, prohibited_unless, prohibits |
| Exclusion | exclude, exclude_if, exclude_unless, exclude_with, exclude_without |
| Accepted / declined | accepted, accepted_if, declined, declined_if |
| Types | string, integer, numeric, decimal, boolean, array, list, file, image, json |
| String constraints | alpha, alpha_dash, alpha_num, ascii, lowercase, uppercase, starts_with, doesnt_start_with, ends_with, doesnt_end_with, contains, doesnt_contain, hex_color, regex, not_regex |
| Sized | min, max, between, size (polymorphic — string length / numeric value / array length) |
| Numeric | gt, gte, lt, lte (literal or field reference), multiple_of, digits, digits_between, max_digits, min_digits |
| Dates | date, date_format, date_equals, after, after_or_equal, before, before_or_equal, timezone (handles now / today / tomorrow / yesterday aliases and field references) |
| Format | email, url, active_url, uuid, ulid, ip, ipv4, ipv6, mac_address |
| Membership | in, not_in, enum (resolves backed PHP enums to z.enum([...])) |
| Cross-field | same, different, confirmed, in_array, distinct |
| File | mimes, mimetypes, extensions, dimensions (server-side image-dim check is deferred with a comment) |
| Server-only | exists, unique, current_password (skipped with comment) |
Anything else triggers an Unhandled rule '<name>'… warning at generate time and is skipped.
Limitations
Honest list of what's not done:
- Nested input shapes. Dotted FormRequest rules like
items.*.qtyare skipped — the generated schema treatsitemsas an unspecified array. To validate nested input shapes, point the field at a Data class andrequest->validate()server-side, then have the client construct the same Data via its own schema. active_urlDNS check is server-only. The package emits.url()plus a// active_url: DNS-resolution check is server-onlycomment.dimensionsfor images needs an asyncImage()load to verify width/height. Currently emitted as a no-op refine with a server-side-only comment. Usemimes/extensionsfor client gating.- Locale fallbacks only walk validation.php's exact key + en. Custom locale message overrides via
validation-inline.phparen't read. - TypeScript inference for circular schemas can fall back to
unknownin deep cases. Zod 4 handles most cases viaz.lazy(), but if you hit a stubborn one, declare the recursive type alias by hand.
Architecture
Two-pass generation:
- Discovery pass — walk
scanpaths, find every#[ZodSchema]-attributed class, register<FQN> → <ExportName>in theSchemaRegistry. - Render pass — analyze each class (Data → reflection of typed props; FormRequest →
rules()array fed throughRuleTranslator), build anObjectSchema, topologically sort with cycle detection, mark back-edges withuseLazyReference, render.
Contributing
Bug reports and PRs welcome. Things that would be useful next:
- Nested FormRequest input schemas (
items.*.qty) - A way to attach a hand-written
superRefineto a Data class (cross-field on outputs) - Pluggable writers (Yup, Valibot, ArkType, …) — the renderer is the only thing that changes
License
MIT.
All versions of laravel-zod with dependencies
illuminate/console Version ^11.0|^12.0|^13.0
illuminate/contracts Version ^11.0|^12.0|^13.0
illuminate/support Version ^11.0|^12.0|^13.0
illuminate/translation Version ^11.0|^12.0|^13.0
illuminate/validation Version ^11.0|^12.0|^13.0