Skip to content

fix(schema-compat): apply .default() / .optional() semantics when models return null#17656

Open
Abuhaithem wants to merge 2 commits into
mastra-ai:mainfrom
Abuhaithem:fix/schema-compat-optional-default-null-handling
Open

fix(schema-compat): apply .default() / .optional() semantics when models return null#17656
Abuhaithem wants to merge 2 commits into
mastra-ai:mainfrom
Abuhaithem:fix/schema-compat-optional-default-null-handling

Conversation

@Abuhaithem

@Abuhaithem Abuhaithem commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Fixes #17655

Problem

In strict structured-output / tool-calling mode, providers return null for a non-required field
the model chose not to fill. Mastra’s schema-compat layer didn’t consistently translate that null
back 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:

  1. processToAISDKSchema built the model-facing schema from Zod’s output projection
    (standardSchemaToJSONSchema(compat) defaults to io: 'output'), where .default() fields are
    required and not tagged x-optional. This contradicts the documented intent of
    processToJSONSchema / toJSONSchema (“Use input mode so fields with defaults are optional”).
  2. Only OpenAI’s #traverse normalized the strict-mode null back to undefined.

The null-handling tests lived in createOpenAISuite (OpenAI only), so the gap was invisible for the
other providers.

Fix

  • Use the input projection for AI-facing schemas: pass { io: 'input' } to
    standardSchemaToJSONSchema in every provider processToAISDKSchema and in
    applyCompatLayer’s jsonSchema branch. Tool output schemas keep io: 'output'.
  • Add a shared convertOptionalNullsToUndefined helper that converts null → undefined for
    optional fields, honoring both conventions — x-optional (OpenAI strict) and absence from
    required (standard JSON Schema) — while preserving explicit null for .nullable() fields. It
    resolves nullable anyOf wrappers and recurses into arrays/nested objects.
  • Wire the helper into Google, Anthropic, DeepSeek and Meta validation (after the existing
    date-converting #traverse). OpenAI already normalizes inline and only needed the projection fix.
  • Close the coverage gap: move the null-handling round-trip into the universal createSuite
    (runs for all providers) and add unit tests for the helper.

Before / after

const schema = z.object({ query: z.string(), limit: z.number().default(10) });
const aiSchema = new GoogleSchemaCompatLayer(model).processToAISDKSchema(schema);

aiSchema.validate({ query: 'cats', limit: null });
// before → { success: false, error: "expected number, received null" }
// after  → { success: true, value: { query: 'cats', limit: 10 } }

Provider matrix (manual repro, { field: null } → validate)

optional default nested default array default nullable keeps null
OpenAI
Google
Anthropic
DeepSeek
Meta

(Every cell was ❌ for .default() and ❌ for non-OpenAI .optional() before this PR.)

Tests

  • pnpm --filter @mastra/schema-compat test863 passed (+61 new), 9 skipped.
  • New shared block in createSuite exercises optional/default/nullable null handling for all providers.
  • New unit tests for convertOptionalNullsToUndefined (both optionality conventions, nullable
    preservation, anyOf resolution, array/nested recursion).
  • Lint + typecheck clean.

Notes for reviewers

  • Behavior-only change; no public API additions beyond exporting the new helper.
  • Downstream @mastra/core: model-facing JSON Schema for .default() fields now reads as optional
    rather 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 null to mean "I didn't provide this." The validation system was treating that null as a bad value instead of recognizing it as "missing," so default values weren't being applied. This PR teaches the system to understand that null from 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-compat when AI models return null for 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-returned null to undefined for optional/default fields before Zod validation, allowing .default() and .optional() semantics to work correctly.

Changes by File

packages/schema-compat/src/index.ts:

  • Exports new utility convertOptionalNullsToUndefined alongside existing wrapSchemaWithNullTransform

packages/schema-compat/src/null-to-undefined.ts:

  • Introduces resolveNullableVariant helper to unwrap nullable anyOf schemas
  • Adds new exported convertOptionalNullsToUndefined function that mutates objects/arrays by converting null to undefined for fields treated as optional (identified via required array absence or x-optional marker), while preserving null for explicitly required or .nullable() fields
  • Preserves existing wrapSchemaWithNullTransform function

packages/schema-compat/src/provider-compats/openai.ts:

  • Updates processToAISDKSchema to explicitly use input projection ({ io: 'input' }) when generating the JSON schema
  • Ensures .default() fields are treated as optional in the schema sent to the model

packages/schema-compat/src/provider-compats/google.ts:

  • Uses input projection in processToAISDKSchema
  • Applies convertOptionalNullsToUndefined during jsonSchema validation to normalize null values back to undefined before Zod parsing

packages/schema-compat/src/provider-compats/anthropic.ts:

  • Uses input projection in processToAISDKSchema
  • Integrates convertOptionalNullsToUndefined alongside date-handling traversal during validation

packages/schema-compat/src/provider-compats/deepseek.ts:

  • Uses input projection and applies convertOptionalNullsToUndefined during JSON schema validation

packages/schema-compat/src/provider-compats/meta.ts:

  • Uses input projection in processToAISDKSchema
  • Applies convertOptionalNullsToUndefined after traversal but before Zod validation

packages/schema-compat/src/utils.ts:

  • Updates applyCompatLayer to use input projection when calling standardSchemaToJSONSchema in jsonSchema mode for both primary compat layer and fallback paths

packages/schema-compat/src/provider-compats/test-suite.ts:

  • Adds comprehensive test coverage for null-handling across all providers via a new test block in createSuite
  • Tests verify: null is treated as undefined for optional fields, .default() values apply when model returns null, null is 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:

  • Adds changeset documentation describing the fix and before/after behavior with a TypeScript example

Results

  • Schema-compat test suite: 863 passed (+61), 9 skipped
  • All providers (OpenAI, Google, Anthropic, DeepSeek, Meta) now consistently handle optional/default fields when models return null
  • Linting and type checking pass

@changeset-bot

changeset-bot Bot commented Jun 7, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6b7e224

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 27 packages
Name Type
@mastra/schema-compat Patch
mastracode Patch
@mastra/agent-builder Patch
@mastra/core Patch
@mastra/editor Patch
@mastra/memory Patch
@internal/playground Patch
@mastra/server Patch
@mastra/voice-google-gemini-live Patch
@mastra/voice-openai-realtime Patch
@mastra/voice-xai-realtime Patch
@mastra/client-js Patch
@mastra/mcp-docs-server Patch
@mastra/opencode Patch
@mastra/longmemeval Patch
@mastra/express Patch
@mastra/fastify Patch
@mastra/hono Patch
@mastra/koa Patch
@mastra/nestjs Patch
mastra Patch
@mastra/deployer-cloud Patch
@mastra/react Patch
@mastra/playground-ui Patch
@mastra/deployer Patch
create-mastra Patch
@mastra/temporal Patch

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

@vercel

vercel Bot commented Jun 7, 2026

Copy link
Copy Markdown

@Abuhaithem is attempting to deploy a commit to the Mastra Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 27312e43-0116-4cc1-9dd9-4e057c614eaf

📥 Commits

Reviewing files that changed from the base of the PR and between 1984efb and 6b7e224.

📒 Files selected for processing (1)
  • .changeset/schema-compat-optional-default-null.md
✅ Files skipped from review due to trivial changes (1)
  • .changeset/schema-compat-optional-default-null.md

Walkthrough

Fixes validation when models return null for .optional() or .default() Zod fields by adding a convertOptionalNullsToUndefined utility, switching provider JSON Schema generation to the input projection, and normalizing nulls to undefined during provider validation.

Changes

Optional/Default Field Null Handling

Layer / File(s) Summary
Core null-to-undefined utility
packages/schema-compat/src/null-to-undefined.ts, packages/schema-compat/src/index.ts
Adds resolveNullableVariant to unwrap nullable JSON Schema variants and convertOptionalNullsToUndefined to recursively convert nullundefined for keys treated as optional/missing from required. Re-exported from package entrypoint.
Provider validation integration
packages/schema-compat/src/provider-compats/anthropic.ts, .../deepseek.ts, .../google.ts, .../meta.ts, .../openai.ts, packages/schema-compat/src/utils.ts
All provider compat layers now generate JSON Schema with { io: 'input' } and apply convertOptionalNullsToUndefined after traversal so Zod sees undefined for omitted/defaulted fields and can apply .optional()/.default() semantics. applyCompatLayer uses input projection in jsonSchema mode.
Test coverage and documentation
packages/schema-compat/src/provider-compats/test-suite.ts, .changeset/schema-compat-optional-default-null.md
Adds tests validating null→undefined conversion, default application for scalars/nested/array elements, preservation of explicit null for required nullable fields, and a changeset showing before/after TypeScript example.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

Suggested labels

tests: green ✅, complexity: critical

Suggested reviewers

  • abhiaiyer91
  • roaminro
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main fix: handling .default() / .optional() semantics when models return null, which directly addresses the core issue.
Linked Issues check ✅ Passed All coding objectives from issue #17655 are fully met: input projection fix applied across all providers, convertOptionalNullsToUndefined helper implemented and wired into Google/Anthropic/DeepSeek/Meta validation, null→undefined conversion preserves .nullable() fields, and universal test suite covers all providers.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the null-handling issue across schema-compat providers; no unrelated modifications detected beyond the documented objectives.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@dane-ai-mastra dane-ai-mastra Bot added the complexity: medium Medium-complexity PR label Jun 7, 2026
@dane-ai-mastra

dane-ai-mastra Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

PR triage

Linked 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

Factor Value Score impact
Files changed 10 +20
Lines changed 230 +12
Author merged PRs 0 -0
Test files changed No -0
Final score 32

Applied label: complexity: medium


Changed test gate

Changed Test Gate is pending. The Changed Test Gate / changed-tests check will update the test label when it completes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 9d7188b and 1984efb.

📒 Files selected for processing (10)
  • .changeset/schema-compat-optional-default-null.md
  • packages/schema-compat/src/index.ts
  • packages/schema-compat/src/null-to-undefined.ts
  • packages/schema-compat/src/provider-compats/anthropic.ts
  • packages/schema-compat/src/provider-compats/deepseek.ts
  • packages/schema-compat/src/provider-compats/google.ts
  • packages/schema-compat/src/provider-compats/meta.ts
  • packages/schema-compat/src/provider-compats/openai.ts
  • packages/schema-compat/src/provider-compats/test-suite.ts
  • packages/schema-compat/src/utils.ts

Comment thread .changeset/schema-compat-optional-default-null.md Outdated

@wardpeet wardpeet left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

complexity: medium Medium-complexity PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Tool-call & structured-output validation fails for .default() / .optional() fields when the model returns null (most providers)

2 participants