fix(schema-compat): apply .default() / .optional() semantics when models return null#17656
Conversation
🦋 Changeset detectedLatest commit: 6b7e224 The changes in this PR will be included in the next version bump. This PR includes changesets to release 27 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
@Abuhaithem is attempting to deploy a commit to the Mastra Team on Vercel. A member of the Team first needs to authorize it. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
✅ Files skipped from review due to trivial changes (1)
WalkthroughFixes validation when models return ChangesOptional/Default Field Null Handling
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
PR triageLinked issue check passed (#17655). Mastra uses CodeRabbit for automated code reviews. Please address all feedback from CodeRabbit by either making changes to your PR or leaving a comment explaining why you disagree with the feedback. Since CodeRabbit is an AI, it may occasionally provide incorrect feedback. PR complexity score
Applied label: Changed test gateChanged Test Gate is pending. The |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.changeset/schema-compat-optional-default-null.md:
- Around line 7-10: Rewrite the changeset copy to be outcome-first and
user-facing: replace implementation jargon in lines mentioning
processToAISDKSchema and convertOptionalNullsToUndefined with a concise
statement of behavior change (e.g., "Defaulted and optional fields are now
optional in AI SDK schemas, and providers returning null for optional/default
fields are normalized to undefined while explicit nullable fields still accept
null"), remove references to Zod's output/input projections and compatibility
layers, and keep examples to a minimum by only mentioning .default(),
.optional(), and .nullable() to clarify which field types are affected.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 90bc21f0-7df3-49b1-bfb3-4dca73773206
📒 Files selected for processing (10)
.changeset/schema-compat-optional-default-null.mdpackages/schema-compat/src/index.tspackages/schema-compat/src/null-to-undefined.tspackages/schema-compat/src/provider-compats/anthropic.tspackages/schema-compat/src/provider-compats/deepseek.tspackages/schema-compat/src/provider-compats/google.tspackages/schema-compat/src/provider-compats/meta.tspackages/schema-compat/src/provider-compats/openai.tspackages/schema-compat/src/provider-compats/test-suite.tspackages/schema-compat/src/utils.ts
wardpeet
left a comment
There was a problem hiding this comment.
I don't belive this is the correct fix, waiting your answer on the issue. We should only replace it if the model made a mistake which you don't really know with your approach
Fixes #17655
Problem
In strict structured-output / tool-calling mode, providers return
nullfor a non-required fieldthe model chose not to fill. Mastra’s schema-compat layer didn’t consistently translate that
nullback into the “absent” value Zod expects, so validation threw
expected <type>, received null..default()fields broke on every provider..optional()fields broke on every provider except OpenAI.Two root causes:
processToAISDKSchemabuilt the model-facing schema from Zod’s output projection(
standardSchemaToJSONSchema(compat)defaults toio: 'output'), where.default()fields arerequired and not tagged
x-optional. This contradicts the documented intent ofprocessToJSONSchema/toJSONSchema(“Use input mode so fields with defaults are optional”).#traversenormalized the strict-modenullback toundefined.The null-handling tests lived in
createOpenAISuite(OpenAI only), so the gap was invisible for theother providers.
Fix
{ io: 'input' }tostandardSchemaToJSONSchemain every providerprocessToAISDKSchemaand inapplyCompatLayer’sjsonSchemabranch. Tool output schemas keepio: 'output'.convertOptionalNullsToUndefinedhelper that convertsnull → undefinedforoptional fields, honoring both conventions —
x-optional(OpenAI strict) and absence fromrequired(standard JSON Schema) — while preserving explicitnullfor.nullable()fields. Itresolves nullable
anyOfwrappers and recurses into arrays/nested objects.date-converting
#traverse). OpenAI already normalizes inline and only needed the projection fix.createSuite(runs for all providers) and add unit tests for the helper.
Before / after
Provider matrix (manual repro,
{ field: null }→ validate)(Every cell was ❌ for
.default()and ❌ for non-OpenAI.optional()before this PR.)Tests
pnpm --filter @mastra/schema-compat test→ 863 passed (+61 new), 9 skipped.createSuiteexercises optional/default/nullable null handling for all providers.convertOptionalNullsToUndefined(both optionality conventions, nullablepreservation, anyOf resolution, array/nested recursion).
Notes for reviewers
@mastra/core: model-facing JSON Schema for.default()fields now reads as optionalrather than required (strictly more correct). Snapshot tests that capture generated tool schemas
with defaults may need regeneration — no runtime behavior regresses.
Changeset
.changeset/schema-compat-optional-default-null.md(@mastra/schema-compat, patch).ELI5
When AI models are asked to fill in structured forms (like tool inputs), sometimes they skip optional fields by sending back
nullto mean "I didn't provide this." The validation system was treating thatnullas a bad value instead of recognizing it as "missing," so default values weren't being applied. This PR teaches the system to understand thatnullfrom the model means "this field is missing" for optional or defaulted fields, so defaults work properly.Overview
This PR fixes cross-provider validation failures in
@mastra/schema-compatwhen AI models returnnullfor non-required fields in strict structured-output and tool-calling scenarios. The fix consists of two main strategies: (1) using the input projection when generating AI-facing JSON schemas so that.default()and optional fields are properly recognized, and (2) introducing a shared utility to convert model-returnednulltoundefinedfor optional/default fields before Zod validation, allowing.default()and.optional()semantics to work correctly.Changes by File
packages/schema-compat/src/index.ts:
convertOptionalNullsToUndefinedalongside existingwrapSchemaWithNullTransformpackages/schema-compat/src/null-to-undefined.ts:
resolveNullableVarianthelper to unwrap nullableanyOfschemasconvertOptionalNullsToUndefinedfunction that mutates objects/arrays by convertingnulltoundefinedfor fields treated as optional (identified viarequiredarray absence orx-optionalmarker), while preservingnullfor explicitly required or.nullable()fieldswrapSchemaWithNullTransformfunctionpackages/schema-compat/src/provider-compats/openai.ts:
processToAISDKSchemato explicitly use input projection ({ io: 'input' }) when generating the JSON schema.default()fields are treated as optional in the schema sent to the modelpackages/schema-compat/src/provider-compats/google.ts:
processToAISDKSchemaconvertOptionalNullsToUndefinedduringjsonSchemavalidation to normalizenullvalues back toundefinedbefore Zod parsingpackages/schema-compat/src/provider-compats/anthropic.ts:
processToAISDKSchemaconvertOptionalNullsToUndefinedalongside date-handling traversal during validationpackages/schema-compat/src/provider-compats/deepseek.ts:
convertOptionalNullsToUndefinedduring JSON schema validationpackages/schema-compat/src/provider-compats/meta.ts:
processToAISDKSchemaconvertOptionalNullsToUndefinedafter traversal but before Zod validationpackages/schema-compat/src/utils.ts:
applyCompatLayerto use input projection when callingstandardSchemaToJSONSchemainjsonSchemamode for both primary compat layer and fallback pathspackages/schema-compat/src/provider-compats/test-suite.ts:
createSuitenullis treated asundefinedfor optional fields,.default()values apply when model returnsnull,nullis preserved for required nullable fields, nested object and array defaults work correctly, and provided non-null values aren't overwritten.changeset/schema-compat-optional-default-null.md:
Results
null