feat: default custom scalar variables to string type in JSON schema with per-scalar overrides - #1625
Closed
asoorm wants to merge 9 commits into
Closed
Conversation
Custom scalars previously mapped to an untyped {} schema, which MCP/LLM
tool consumers reject (properties must declare a type). Default to
"string" — the wire format of virtually all opaque scalars.
The test 'custom scalar types are represented as objects' was renamed to 'custom scalar variables with descriptions default to string type' to accurately reflect the test assertion that custom scalars emit "type": ["string", "null"] rather than an empty object.
Callers can override the JSON schema per custom scalar name; unmapped custom scalars default to string and are reported via DefaultedScalars so integrators (e.g. Cosmo router) can log a startup warning.
Add a subtest using the same overridden scalar at two different non-null contexts in one operation. Without Clone() the second variable's Nullable mutation leaks into the first via the shared override pointer; sanity-checked by temporarily removing the Clone() call locally and confirming the new subtest fails.
Contributor
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
Contributor
Author
|
Closing. The schema generator now lives in the Cosmo router as an internal package (wundergraph/cosmo#3147). The library package stays unchanged. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
BuildJsonSchema(v2/pkg/engine/jsonschema) maps custom scalar variables ($after: Cursor) to an untyped{}schema. Downstream, that shape lands in MCP toolinputSchema, where properties without a"type"are rejected or degraded by strict LLM tool consumers (Anthropic marketplace submission validation, Claude Code >=2.0.21 client-side schema validation, OpenAI strict mode, GitHub Copilot CLI). A GraphQL operation using any custom scalar as a variable currently produces a tool the model can't reason about and some clients refuse outright.Existing GraphQL-to-MCP implementations don't solve this out of the box: unmapped custom scalars are left as
{}or hardcoded to{"type":"object"}- the latter actively wrong for string-serialized scalars.Changes
"type": "string"(["string","null"]when nullable) - the wire format of virtually all opaque scalars (cursors, IDs, dates, URLs). The unknown-node fallback still returns the untyped any-schema; built-in scalars are unaffected.WithScalarSchemas(map[string]*JsonSchema)variadic option onBuildJsonSchema/NewVariablesSchemaBuilderfor per-scalar overrides (e.g.JSON->{"type":"object"},BigInt->{"type":"integer"}). Built-in scalars cannot be overridden. Override schemas are deep-cloned per use: the builder mutatesNullableon returned schemas, so a shared map value would leak nullability between variables - guarded by a regression test that fails if the clone is dropped.(*VariablesSchemaBuilder).DefaultedScalars() []string(sorted, unique) reporting which custom scalars fell back to the default, so integrators can log a startup warning naming missing mappings.(*JsonSchema).Clone(), nil-safe.Behavioral change (intentional)
Output for custom scalars changes from
{}to"type": "string". Consumers that validate inputs against the generated schema will start rejecting non-string values for unmapped custom scalars;WithScalarSchemasis the escape hatch. Signature additions are variadic - call sites compile unchanged, but code holdingBuildJsonSchemaas a typed function value needs the new signature. Minor version bump.Usage
Limitations
variables_schema.go,EnterVariableDefinition). An object-mapped override on a nullable variable therefore loses itsnullunion. Deliberately untouched here; will be documented at the integration layer.Deliberately not in scope
[Cursor], SDL-description fallback, built-in-name-in-overrides ignored) - ~30 lines, follow-up.Test plan
go test ./pkg/engine/jsonschema/ -run 'TestBuildJsonSchema' -v- includes: custom scalar defaults (nullable + non-null, variable + nested input field), override mapping, same-overridden-scalar-at-two-nullabilities (fails ifClone()is removed - verified by mutation),DefaultedScalarsdedup + sort.grep -rn "NewAnySchema()" v2/pkg/engine/jsonschema/variables_schema.go-> exactly one hit, the unknown-node fallback.grep -rn "engine/jsonschema" --include="*.go" v2/ | grep -v pkg/engine/jsonschema-> no in-repo consumers; the API addition breaks nothing internally.Fixes ENG-9903.