fix(codegen): distinguish nullable from optional in zod and valibot schemas#70
Merged
Torkan merged 1 commit intoash-project:mainfrom Apr 29, 2026
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #69
Note
While I think this is the correct behavior, it could be considered a breaking change as it'll change any previously generated schemas. I don't think any schemas which previously worked will NOT work anymore, though (for Zod at least, not sure about Valibot). The fix is actually more permissive, as they'll now correctly accept
nullin addition to what they already accepted before (a valid value orundefined).tl;dr
Generated zod and valibot schemas rejected
nullfor nullable Ash attributes because the codegen conflated "may be omitted" with "may be null" into a single.optional()wrapper. This PR splits the two concepts, so nullable+omittable fields now emitz.T().nullable().optional()(zod) andv.optional(v.nullable(v.T()))(valibot), and the TypeScript RPC argument types now thread| nullfor nullable arguments.What changed
wrap_nullable/1callback toSchemaFormatterbehaviorZodSchemaGenerator(.nullable()) andValibotSchemaGenerator(v.nullable(...))SchemaCore.maybe_wrap_nullable_optional/4helper, applied at five conflation sites: typed-container fields, action arguments, accept fields (create + update/destroy), resource-level schemas, and the typed-controller route schema| nullthrough the four TS argument-emitter paths ininput_types.exand the typed-controller TS query/input paths:createaccept fields withallow_nil_inputto admitnullon both the TS and zod sides (previously only zod was correct after the split)Breaking change
The fix widens inferred TypeScript types for nullable fields:
z.infer<typeof Schema>for nullable+omittable fields gains| nullInferInput/InferOutputlikewise widensmetadata?: Record<string, any>becomemetadata?: Record<string, any> | nullNo previously-valid input fails to validate;
nullis now additionally accepted where the underlying Ash semantics permit it. Consumer code that assumed non-null when handling inferred types may surface new compile errors — those errors typically reveal latent bugs (the field could already benullat runtime), they don't introduce new ones.Contributor checklist
Leave anything that you believe does not apply unchecked.