Skip to content

fix(codegen): distinguish nullable from optional in zod and valibot schemas#70

Merged
Torkan merged 1 commit intoash-project:mainfrom
mike1o1:nullable-vs-optional
Apr 29, 2026
Merged

fix(codegen): distinguish nullable from optional in zod and valibot schemas#70
Torkan merged 1 commit intoash-project:mainfrom
mike1o1:nullable-vs-optional

Conversation

@mike1o1
Copy link
Copy Markdown
Contributor

@mike1o1 mike1o1 commented Apr 29, 2026

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 null in addition to what they already accepted before (a valid value or undefined).


tl;dr

Generated zod and valibot schemas rejected null for 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 emit z.T().nullable().optional() (zod) and v.optional(v.nullable(v.T())) (valibot), and the TypeScript RPC argument types now thread | null for nullable arguments.

What changed

  • Added wrap_nullable/1 callback to SchemaFormatter behavior
  • Implemented in ZodSchemaGenerator (.nullable()) and ValibotSchemaGenerator (v.nullable(...))
  • Added SchemaCore.maybe_wrap_nullable_optional/4 helper, applied at five conflation sites: typed-container fields, action arguments, accept fields (create + update/destroy), resource-level schemas, and the typed-controller route schema
  • Threaded | null through the four TS argument-emitter paths in input_types.ex and the typed-controller TS query/input paths
  • Fixed :create accept fields with allow_nil_input to admit null on 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 | null
  • Valibot's InferInput / InferOutput likewise widens
  • RPC argument types like metadata?: Record<string, any> become metadata?: Record<string, any> | null

No previously-valid input fails to validate; null is 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 be null at runtime), they don't introduce new ones.

Contributor checklist

Leave anything that you believe does not apply unchecked.

  • I accept the AI Policy, or AI was not used in the creation of this PR.
  • Bug fixes include regression tests
  • Chores
  • Documentation changes
  • Features include unit/acceptance tests
  • Refactoring
  • Update dependencies

@Torkan Torkan merged commit f5ae623 into ash-project:main Apr 29, 2026
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generated Zod schemas dont distinguish nullable and optional

2 participants