Skip to content

Report defaultValue in introspection as a GraphQL literal - #625

Open
xperiandri wants to merge 2 commits into
reflection-type-comparisonfrom
introspection-default-value-literals
Open

xperiandri wants to merge 2 commits into
reflection-type-comparisonfrom
introspection-default-value-literals

Conversation

@xperiandri

@xperiandri xperiandri commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator

Introspection reported the defaultValue of an argument or an input object field serialized to JSON with the schema's JsonOptions, while the spec defines it as "a GraphQL-formatted string" (§4.2.3). JSON is a valid GraphQL literal only by chance: an enum value became its .NET value, such as {"Case":"Local"} for an F# union case, the keys of an input object were quoted, and the output coercion of a scalar was skipped. Clients that build a schema from introspection, like graphql-js buildClientSchema used by graphql-codegen, parse every defaultValue and failed with Syntax Error: Expected Name, found String "Case".

Fixes #623. Builds on #628, which this PR targets; it recognizes skippable values through the type checks of #628. Every commit builds on its own, so the PR is meant to be merged with rebase.

Changes

  • The new internal ValueLiterals module of FSharp.Data.GraphQL.Shared prints a value by its input type, like astFromValue and print of graphql-js, and Schema reports default values with it:
    • an enum value as the name of its EnumValue, e.g. LOCAL;
    • a scalar value as its output coercion serializes it: numbers, booleans and escaped strings directly, and any other type, like a date, a GUID or a URI, the way the schema's JsonOptions serialize it;
    • a list with every item printed by the item type, e.g. [LOCAL, REMOTE];
    • an input object with the GraphQL names of its fields, read through the properties the fields bind to, leaving skipped SkippableInput fields out, e.g. {userId: "1", attendanceMode: REMOTE};
    • an option or voption unwrapped, and a missing one as null.
  • A default value without a GraphQL literal (a value outside of its enum, NaN or an infinity, a value its scalar cannot or refuses to serialize, an input object without a property for one of its fields) is no longer reported, and its input stays nullable. Printing never throws: the introspected schema backs the validation of every request, so one bad default value would fail them all.
  • Document.ToQueryString printed a string value between quotes as it was, so a string with a quote, a backslash or a control character produced an invalid document. The string printer moves from ValueLiterals to AstExtensions as appendStringValue, and both use it.

Notes

  • The format follows print of graphql-js 16: {a: 1} and [1, 2], without the inner spaces ToQueryString puts in.
  • An ID is always printed as a string, while graphql-js prints an ID that looks like an integer as an Int; both are valid ID input.
  • Besides the control characters, strings escape U+2028 and U+2029, which the parser of this library does not accept unescaped in a string.
  • Booleans, integers and plain strings print as before, so the introspection snapshots of the integration tests do not change; the integration tests were not run.

Verification

🤖 Generated with Claude Code

Copilot AI balanced review requested due to automatic review settings October 3, 2026 00:53

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

Recursive defaults can overflow the stack, and enum matching uses equality semantics inconsistent with runtime serialization.

Review effort: Balanced
Findings: 1 High severity · 1 Medium severity

Open (2)
What changed in this PR

Fixes #623 by formatting introspection default values as valid GraphQL literals rather than JSON.

Changes:

  • Adds type-aware literal formatting for scalars, enums, lists, and input objects.
  • Integrates formatting into schema introspection and fixes option-array unwrapping.
  • Adds regression and unit coverage plus release notes.
File Description
src/​FSharp.Data.GraphQL.Shared/​ValueLiterals.fs Implements GraphQL literal formatting.
src/​FSharp.Data.GraphQL.Shared/​Helpers/​Reflection.fs Corrects option type detection.
src/​FSharp.Data.GraphQL.Shared/​FSharp.Data.GraphQL.Shared.fsproj Includes the new module.
src/​FSharp.Data.GraphQL.Server/​Schema.fs Uses literals for introspection defaults.
tests/​FSharp.Data.GraphQL.Tests/​ValueLiteralsTests.fs Tests literal formatting behavior.
tests/​FSharp.Data.GraphQL.Tests/​IntrospectionTests.fs Adds introspection regressions.
tests/​FSharp.Data.GraphQL.Tests/​FSharp.Data.GraphQL.Tests.fsproj Includes the new tests.
RELEASE_NOTES.md Documents both fixes.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +250 to +252
|> tryAppendSeparated builder (fun struct (field, fieldValue) ->
builder.Append(field.Name).Append(": ") |> ignore
tryAppendValue jsonOptions builder field.TypeDef fieldValue)
| Enum enumDef ->
match
enumDef.Options
|> Array.vtryFind (fun enumValue -> enumValue.Value = value)
@github-actions

github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Test Results

    9 files      9 suites   14m 47s ⏱️
  899 tests   894 ✅  5 💤 0 ❌
2 697 runs  2 682 ✅ 15 💤 0 ❌

Results for commit d3a9152.

♻️ This comment has been updated with latest results.

@xperiandri
xperiandri force-pushed the introspection-default-value-literals branch from 328530f to d3a9152 Compare October 4, 2026 14:46
@xperiandri
xperiandri changed the base branch from dev to reflection-type-comparison October 4, 2026 14:46
@xperiandri
xperiandri force-pushed the reflection-type-comparison branch from faa08eb to cd7e32d Compare October 4, 2026 20:19
xperiandri and others added 2 commits October 4, 2026 22:20
The `defaultValue` of `__InputValue` is a GraphQL-formatted string, but the
schema serialized the default value to JSON with its `JsonOptions`, which
is a valid GraphQL literal only by chance: an enum value became its .NET
value, such as `{"Case":"Local"}` for an F# union case, the keys of an
input object were quoted, and the output coercion of a scalar was skipped.
Clients that build a schema from introspection, like graphql-js
`buildClientSchema`, parse every `defaultValue` and failed on it.

The new internal `ValueLiterals` module prints a value by its input type,
like `astFromValue` and `print` of graphql-js:

* an enum value as the name of its `EnumValue`;
* a scalar value as its output coercion serializes it: numbers, booleans
  and escaped strings directly, and any other type, like a date, a GUID or
  a URI, the way the JSON options of the schema serialize it;
* a list with every item printed by the item type, and a single value as
  that item, which input coercion accepts in place of a list;
* an input object with the GraphQL names of its fields, read through the
  properties the fields bind to, leaving the skipped skippable fields out;
* an `option` or `voption` unwrapped, and a missing one as `null`.

A default value without a GraphQL literal, such as a value outside of its
enum, a `NaN` float, a value its scalar cannot serialize or an input object
without a property for one of its fields, is not reported, while its input
stays nullable. Printing never throws, because the introspected schema
backs the validation of every request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`Document.ToQueryString` printed a string value between quotes as it was,
so a string with a quote, a backslash or a control character produced an
invalid document. It now escapes them with `appendStringValue`, the string
printer of `ValueLiterals`, which moves to `AstExtensions` so that both
use it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@xperiandri
xperiandri force-pushed the introspection-default-value-literals branch from d3a9152 to 5e95032 Compare October 4, 2026 20:22

This branch has not been deployed

No deployments
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.

Introspection emits JSON-serialized value as defaultValue instead of a GraphQL literal (breaks enum defaults)

2 participants