Skip to content

fix(jsonschema): align nullability with JSON Schema 2020-12 - #1518

Merged
asoorm merged 1 commit into
masterfrom
ahmet/jsonschema-2020-12-nullable
Jun 5, 2026
Merged

fix(jsonschema): align nullability with JSON Schema 2020-12#1518
asoorm merged 1 commit into
masterfrom
ahmet/jsonschema-2020-12-nullable

Conversation

@asoorm

@asoorm asoorm commented May 26, 2026

Copy link
Copy Markdown
Contributor

Stacked on top of #1513. Review #1513 first; this PR's diff is only the nullability change.

Problem

The generator emits the OpenAPI 3.0 keyword "nullable": true for nullable fields.

Standard JSON Schema 2020-12 validators (e.g. santhosh-tekuri/jsonschema, used by Cosmo's router) silently ignore unknown keywords, so a payload containing null for an otherwise-valid optional field is rejected at the schema-validation boundary.

LLMs that legitimately emit null for absent optional fields - OpenAI's gpt-4o does this routinely in tool-call arguments - then have their output rejected before reaching the resolver.

Same failure pattern as #1513 (ENG-9631) but different root cause.

Discovered while building the provider-compatibility evidence for that PR.

Fix

Switch nullability to the JSON Schema 2020-12 form:

Schema kind Before (OpenAPI 3.0) After (JSON Schema 2020-12)
typed {"type": "string", "nullable": true} {"type": ["string", "null"]}
enum {"type": "string", "enum": [...], "nullable": true} {"type": ["string", "null"], "enum": [..., null]}
$ref {"$ref": "...", "nullable": true} {"anyOf": [{"$ref": "..."}, {"type": "null"}]}

The "nullable" key is no longer emitted. The internal Nullable field is retained for construction-time logic; only the JSON serialization changes. Definition bodies under $defs are now marked non-nullable explicitly so the def carries the single canonical type and per-use-site nullability is applied via the surrounding ref.

Tests

  • New nullable_2020_12_test.go: validates explicit-null payloads against the generated schema for nullable scalar, enum, and recursive ref fields (red before, green after).
  • Existing golden-JSON tests across schema_test.go and variables_schema_test.go updated to assert the 2020-12 shape.

All package tests pass; gofmt clean; go vet clean.

@coderabbitai

coderabbitai Bot commented May 26, 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: 054612f9-f755-4673-af9a-e12809face3e

📥 Commits

Reviewing files that changed from the base of the PR and between d0ad76b and 6bc6bfc.

📒 Files selected for processing (5)
  • v2/pkg/engine/jsonschema/nullable_2020_12_test.go
  • v2/pkg/engine/jsonschema/schema.go
  • v2/pkg/engine/jsonschema/schema_test.go
  • v2/pkg/engine/jsonschema/variables_schema.go
  • v2/pkg/engine/jsonschema/variables_schema_test.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • v2/pkg/engine/jsonschema/variables_schema.go
  • v2/pkg/engine/jsonschema/nullable_2020_12_test.go
  • v2/pkg/engine/jsonschema/schema.go
  • v2/pkg/engine/jsonschema/schema_test.go
  • v2/pkg/engine/jsonschema/variables_schema_test.go

📝 Walkthrough

Walkthrough

This PR migrates JSON Schema nullability representation from OpenAPI-style "nullable": true to JSON Schema 2020-12 conventions. The core MarshalJSON method in schema.go now emits type unions (["string", "null"]), appends null to enums, and uses anyOf constructs for refs. All dependent tests in schema_test.go and variables_schema_test.go are updated accordingly, with a new compliance test validating the generated output.

Changes

JSON Schema 2020-12 Nullability Format

Layer / File(s) Summary
Serialization contract: Nullable field and MarshalJSON update
v2/pkg/engine/jsonschema/schema.go
The Nullable field becomes internal-only (json:"-"). MarshalJSON now emits type unions ["<type>", "null"] for nullable typed fields, appends null to nullable enums, and serializes nullable refs via anyOf containing the ref and a {"type":"null"} branch.
Schema generation test expectations
v2/pkg/engine/jsonschema/schema_test.go
Test assertions updated to expect nullability via type arrays/unions, null enum entries, and anyOf constructs; validates object schemas, enum schemas, required fields, numeric constraints, and complex nested structures.
Recursive input type definition handling
v2/pkg/engine/jsonschema/variables_schema.go
During recursive ensureDef finalization, the processed input-object body is stored with Nullable = false, ensuring definitions themselves remain non-nullable.
Variables schema test expectations
v2/pkg/engine/jsonschema/variables_schema_test.go
All test assertions updated to expect nullability via type arrays, enum null values, and anyOf constructs across simple/nested input objects, scalar arguments, deeply nested types, recursive types, and root schema cases.
JSON Schema 2020-12 compliance test
v2/pkg/engine/jsonschema/nullable_2020_12_test.go
New test validates that generated schemas compile with a strict JSON Schema 2020-12 validator and correctly accept explicit null values for nullable scalars, enums, and recursive refs.

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • wundergraph/graphql-go-tools#1513: Touches v2/pkg/engine/jsonschema/variables_schema.go recursive input schema generation and relates to $ref/$defs recursive-type emission changes.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(jsonschema): align nullability with JSON Schema 2020-12' clearly and concisely summarizes the main change: updating JSON Schema nullability encoding from OpenAPI 3.0 style to JSON Schema 2020-12 standard.
Description check ✅ Passed The description provides relevant context explaining the problem (OpenAPI nullable ignored by standard validators), the fix (JSON Schema 2020-12 encoding), example transformations, and testing approach—all directly related to the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ahmet/jsonschema-2020-12-nullable

Warning

Review ran into problems

🔥 Problems

Stopped waiting for pipeline failures after 30000ms. One of your pipelines takes longer than our 30000ms fetch window to run, so review may not consider pipeline-failure results for inline comments if any failures occurred after the fetch window. Increase the timeout if you want to wait longer or run a @coderabbit review after the pipeline has finished.


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

@asoorm
asoorm marked this pull request as ready for review May 28, 2026 06:32

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

Base automatically changed from ahmet/eng-9631-json-schema-recursive-refs to master May 29, 2026 19:34
@asoorm
asoorm requested a review from a team as a code owner May 29, 2026 19:34
The generator previously expressed nullable fields with the OpenAPI 3.0
keyword `"nullable": true`. Standard JSON Schema 2020-12 validators (e.g.
santhosh-tekuri/jsonschema, used by Cosmo's router) silently ignore
unknown keywords, so a payload containing `null` for an otherwise valid
optional field is rejected at the schema-validation boundary. LLMs that
legitimately emit `null` for absent optional fields (e.g. OpenAI's GPT-4o
in tool-call arguments) then have their output rejected before reaching
the resolver — same failure pattern as ENG-9631, different root cause.

Switch to JSON Schema 2020-12:
  - typed schemas:  "type": [<type>, "null"]
  - enum schemas:   null appended to "enum"
  - $ref schemas:   {"anyOf": [{"$ref": ...}, {"type": "null"}]}
  - the "nullable" keyword is no longer emitted.

The internal Nullable field is retained for construction-time logic; only
the JSON serialization changes. Definition bodies under $defs are now
marked non-nullable explicitly, so the def carries the single canonical
type and per-use-site nullability is applied via the surrounding ref.

Adds a regression test that validates explicit-null payloads against the
generated schema for nullable scalar, enum, and recursive ref fields,
and updates the existing golden-JSON tests to assert the 2020-12 shape.
@asoorm
asoorm force-pushed the ahmet/jsonschema-2020-12-nullable branch from d0ad76b to 6bc6bfc Compare June 5, 2026 08:03
@asoorm
asoorm merged commit 6fcdf8c into master Jun 5, 2026
10 checks passed
@asoorm
asoorm deleted the ahmet/jsonschema-2020-12-nullable branch June 5, 2026 08:38
asoorm pushed a commit that referenced this pull request Jun 5, 2026
🤖 I have created a release *beep* *boop*
---


##
[2.4.3](v2.4.2...v2.4.3)
(2026-06-05)


### Bug Fixes

* calculate costs for abstract fields without double counting
([#1521](#1521))
([4175a9e](4175a9e))
* **jsonschema:** align nullability with JSON Schema 2020-12
([#1518](#1518))
([6fcdf8c](6fcdf8c))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: wundergraph-bot[bot] <285992168+wundergraph-bot[bot]@users.noreply.github.com>
asoorm added a commit that referenced this pull request Jun 8, 2026
…ct" (#1528)

Follow-up to #1518. That PR was correct but incomplete and exposed an
old defect.

## Problem

`GetSchema()` only forced the **root** variables object non-nullable
when the operation had a required variable:

```go
if len(v.schema.Required) > 0 {
    v.schema.Nullable = false
}
```

Operations with **all-optional variables** kept a nullable root. Before
#1518 that serialized to `{"type":"object","nullable":true}` and was
harmless (validators ignore the unknown `"nullable"` keyword). After
#1518 it serializes to `{"type":["object","null"]}`, which strict
consumers reject - the MCP go-sdk's `AddTool` requires the input schema
`type` to be exactly `"object"` and panics:

```
panic: AddTool "list_employees": input schema must have type "object" (got [object null])
```

This breaks the cosmo router engine bump 2.4.2 -> 2.4.3
(wundergraph/cosmo#2925): `pkg/mcpserver`, `protocol` and `security`
suites all panic.

## Fix

The root variables object is always a concrete object - the container is
present or omitted, never the JSON literal `null` - so set `Nullable =
false` unconditionally in `GetSchema()`. This is consistent with nested
input-object variables, which are already forced non-nullable. Only
individual optional fields remain nullable.

Golden tests updated (root `["object","null"]` -> `"object"`); the
`root_schema_nullable_based_on_required_arguments` subtest renamed to
`root schema is always a non-nullable object`.

## Tests

`go test ./pkg/engine/jsonschema/` passes; gofmt and go vet clean.
Verified against the cosmo router via a local `replace`: `pkg/mcpserver`
no longer panics.

## Follow-up

Needs a patch release (2.4.4) and a corresponding engine bump in
cosmo#2925.

Fixes ENG-9682
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