Skip to content

fix: camelize JSON-schema required values for OpenAI strict structured outputs - #38

Merged
enriclluelles merged 2 commits into
mainfrom
fix/camelize-schema-required
Jul 1, 2026
Merged

fix: camelize JSON-schema required values for OpenAI strict structured outputs#38
enriclluelles merged 2 commits into
mainfrom
fix/camelize-schema-required

Conversation

@GuillenSanti

@GuillenSanti GuillenSanti commented Jul 1, 2026

Copy link
Copy Markdown
Member

Problem

Since the Mastra SDK upgrade (ai 5→6, @ai-sdk/azure 2→3, @mastra/core 1.4→1.45), agents using structured outputs (generate_object) started failing in production with:

Ai::Error: Invalid schema for response_format 'response':
In context=('properties','matches','items'), 'required' is required to be supplied
and to be an array including every key in properties. Missing 'transactionId'.

This is firing across many structured-output agents at once — banking transaction categorization, the finance OCR document scanner, the interactive GraphQL path, performance-review summaries, and more (1,000+ events / 300+ users).

Triggered by: factorialco/factorial-agent#1826 ("MASTRA UPGRADE", merged 2026-07-01) — https://github.com/factorialco/factorial-agent/pull/1826 — which bumped ai 5→6 / @ai-sdk/azure 2→3 / @mastra/core 1.4→1.45, enabling OpenAI strict structured outputs. The largest wave (banking + OCR Sidekiq jobs and more) escalated right after its deploy today; the same schema signature also appears via the interactive GraphQL path going back ~6 days. Either way the upgrade did not introduce the bug — it exposed this latent one, and the root cause + fix are identical for all of them.

Affected Sentry issues (same root cause)

Dashboard — all Invalid schema for response_format errors. Every one has the same signature: required must include every key in properties; Missing '<camelCaseField>'.

Root cause (a latent bug, not the upgrade)

Ai::StructToJsonSchema.convert builds the schema from a T::Struct with snake_case keys in both properties and required — internally consistent.

Ai::Clients::Mastra#deep_camelize_keys then runs deep_transform_keys, which camelizes hash KEYS but not array string VALUES:

  • properties keys → camelCase (transaction_idtransactionId) ✅
  • required values → stay snake_case (transaction_id) ❌ (they are array elements, not keys)

So the schema sent to OpenAI has always been internally inconsistent (properties camelCase, required snake_case).

Why it only breaks now: the pre-upgrade path did not enforce OpenAI strict structured outputs, so the mismatch was tolerated. The upgraded SDK enables strict mode, where OpenAI requires required to list every property key by exact name. The camel/snake mismatch now fails validation. The upgrade did not introduce the bug — it exposed a latent one.

Fix

After deep_transform_keys, recursively align every required array with the camelized property keys (camelize_schema_required!). The walk is recursive so it also handles nested and nilable (anyOf) structs — e.g. the OCR payment_details.account_number case (context=('properties','paymentDetails','anyOf','0'), Missing 'accountNumber'), not just flat top-level schemas.

Verification

  • Reproduced deterministically (no OpenAI call) using the real production struct through the exact path (StructToJsonSchema.convertdeep_camelize_keys):
    • before: properties=[transactionId, categoryId], required=[transaction_id, category_id] → strict REJECTS
    • after: required=[transactionId, categoryId] → strict ACCEPTS
  • Added two regression specs (flat + nested anyOf). Note the pre-existing "converts struct fields to camelCase" spec never asserted required — which is how this slipped through.
  • bundle exec rspec → 85 examples, 0 failures · srb tc → no errors · rubocop → no offenses.

Impact

Fixes structured-output failures across all agents whose struct has multi-word (snake_case) fields — banking categorization, finance OCR, etc. This is a cross-domain production incident; after merge + release, bump the ai gem pin in the monorepo Gemfile.

🤖 Generated with Claude Code

GuillenSanti and others added 2 commits July 1, 2026 12:21
…red outputs

`deep_camelize_keys` camelized hash keys but not the string values inside
`required` arrays, leaving `properties` in camelCase while `required` stayed
snake_case. OpenAI strict structured outputs (enforced since the Mastra SDK
upgrade) reject that mismatch. Recursively align every `required` array with
the camelized property keys (handles nested / anyOf schemas too).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Versioning is handled in dedicated release PRs (e.g. #35) and the monorepo
pins this gem by git SHA, not version — so a patch bump is unnecessary here and
broke frozen-mode `bundle install` in CI (gemspec 0.6.1 vs locked 0.6.0).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@GuillenSanti
GuillenSanti marked this pull request as ready for review July 1, 2026 12:33
@GuillenSanti
GuillenSanti requested a review from a team as a code owner July 1, 2026 12:33
@enriclluelles
enriclluelles merged commit 311fcd6 into main Jul 1, 2026
1 check passed
@enriclluelles
enriclluelles deleted the fix/camelize-schema-required branch July 1, 2026 12:38
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.

2 participants