Skip to content

v0.10.3 - validator honours nullable required fields (OpenAI strict mode unblock)

Choose a tag to compare

@justi justi released this 10 Jun 16:30
· 63 commits to main since this release

Hot-fix. The schema validator now accepts nil on required-but-nullable fields — fixing a conflation bug that has existed in every published version since 0.2.x.

Why this matters

OpenAI's structured-output strict mode requires every property to be present in required. The canonical way to express "nullable" under strict is therefore required + anyOf-null (or type: ["...", "null"]). Pre-0.10.3 our validator wrongly rejected this combination, conflating "required" (must be present) with "non-nullable" (cannot be null) — JSON Schema treats those as orthogonal.

If you bypassed it by setting required: false and disabling strict: true on every nullable field, you can now restore strict: true and keep the field required — that's the standard OpenAI nullable idiom.

Fixed

  • SchemaValidator now honours all three nullability idioms on required fields:
    • type: ["string", "null"] (array form)
    • type: "null" (degenerate scalar)
    • anyOf / oneOf containing a {type: "null"} branch

Strictly additive — non-nullable required fields still reject nil exactly as before. A regression-guard spec pins that behaviour.

Background

Discovered via dogfooding in a production adopter using OpenAI strict structured output with 17 nullable fields ("set the rest to null" prompt). The bug surfaced now and not earlier because:

  • most early-adopter schemas were either fully non-nullable or used required: false for optional fields
  • it only fires in the exact combo (required + nullable schema + LLM returns null)
  • this is precisely the OpenAI strict-mode nullable idiom — so adopters avoiding strict didn't hit it

The matching optional DSL in ruby_llm-schema produces this same shape intentionally (required + anyOf-null), aligned with OpenAI strict mode.

Tests

4 new specs in spec/ruby_llm/contract/contract/schema_validator_spec.rb under "required field with nil value":

  • array-form nullable accepted
  • anyOf-null accepted
  • oneOf-null accepted
  • regression guard: plain non-nullable still rejected

Suite: 1406 examples / 0 failures / 7 pending (was 1402/0/7).

Full diff: v0.10.2...v0.10.3