Skip to content

feat: @coerce schema directive for surgical per-field scalar coercion (Approach 2) - #692

Merged
mendescamara merged 6 commits into
masterfrom
feat/coerce-directive
May 7, 2026
Merged

feat: @coerce schema directive for surgical per-field scalar coercion (Approach 2)#692
mendescamara merged 6 commits into
masterfrom
feat/coerce-directive

Conversation

@mendescamara

Copy link
Copy Markdown
Contributor

Context

GraphQL's built-in scalars (Int, Float, String) reject values that are technically compatible but typed differently — e.g. "20" for an Int argument. A global override (see Approach 1) removes strict validation for the entire schema, which may be undesirable.

This PR explores Approach 2: surgical coercion via a @coerce schema directive that can be applied selectively to individual INPUT_FIELD_DEFINITION or ARGUMENT_DEFINITION locations.

How it works

The directive follows the exact same pattern as @sanitize in node-vtex-api: a SchemaDirectiveVisitor subclass that replaces the field's scalar type at schema-build time with a coercible version.

makeExecutableSchema
  → applySchemaDirectives
    → Coerce.visitInputFieldDefinition(field)
      → field.type = replaceWithCoercible(field.type)   // swap Int → CoercibleInt

replaceWithCoercible walks GraphQLNonNull and GraphQLList wrappers recursively to reach the inner named scalar, then swaps it with the matching coercible instance. Only Int, Float, and String have coercible versions; other types pass through unchanged.

Changes

node/directives/coerce.ts (new)

  • Coerce extends SchemaDirectiveVisitor with visitInputFieldDefinition and visitArgumentDefinition
  • Three coercible scalar instances (same logic as Approach 1, but scoped to this directive)
  • replaceWithCoercible helper that unwraps NonNull/List before substituting

node/directives/index.ts

Registers coerce: Coerce in the schemaDirectives map.

graphql/schema.graphql

Declares the directive:

directive @coerce on INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION

graphql/types/Shipping.graphql

Applies the directive as a concrete example:

# Before
input ShippingItem {
  quantity: String   # typed as String for legacy reasons
}

# After
input ShippingItem {
  quantity: Int @coerce   # semantically correct, still accepts "20"
}

ShippingItem.quantity was previously typed as String even though it represents an integer count. With @coerce it becomes Int (correct semantic type) while retaining backward compatibility for clients that send it as a string.

Trade-offs

Approach 2 (this PR)
Scope Only fields explicitly annotated with @coerce
SDL changes Directive declaration + annotation per field
Risk Minimal — strict validation preserved everywhere else
Maintenance Each new field needs the annotation

Compare with Approach 1 — global scalar override which coerces all fields automatically.

References

  • node-vtex-api @sanitize directive — SchemaDirectiveVisitor replacing field types at build time
  • graphql-tools v3.x SchemaDirectiveVisitor — supports visitInputFieldDefinition and visitArgumentDefinition
  • graphql@0.13.2 scalar resolution flow: parseLiteral for inline values, parseValue for variables

🤖 Generated with Claude Code

Follows the @sanitize pattern from node-vtex-api: the SchemaDirectiveVisitor
replaces the field's scalar type at schema-build time with a coercible version
that accepts compatible-but-differently-typed values in parseValue/parseLiteral.

Applied to ShippingItem.quantity (String → Int @Coerce) as a concrete example.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@mendescamara
mendescamara requested a review from a team as a code owner May 6, 2026 13:34
@mendescamara
mendescamara requested review from gabpaladino, leo-prange-vtex and vsseixaso and removed request for a team May 6, 2026 13:34
@vtex-io-ci-cd

vtex-io-ci-cd Bot commented May 6, 2026

Copy link
Copy Markdown
Contributor

Hi! I'm VTEX IO CI/CD Bot and I'll be helping you to publish your app! 🤖

Please select which version do you want to release:

  • Patch (backwards-compatible bug fixes)

  • Minor (backwards-compatible functionality)

  • Major (incompatible API changes)

And then you just need to merge your PR when you are ready! There is no need to create a release commit/tag.

  • No thanks, I would rather do it manually 😞

@vtex-io-docs-bot

vtex-io-docs-bot Bot commented May 6, 2026

Copy link
Copy Markdown

Beep boop 🤖

I noticed you didn't make any changes at the docs/ folder

  • There's nothing new to document 🤔
  • I'll do it later 😞

In order to keep track, I'll create an issue if you decide now is not a good time

  • I just updated 🎉🎉

@github-actions

github-actions Bot commented May 6, 2026

Copy link
Copy Markdown
Warnings
⚠️

👀 The size of this pull request seems relatively large (>420 modifications). Consider splitting it into smaller pull requests to help make reviews easier and faster.

Generated by 🚫 dangerJS against 4f5d3fa

@mendescamara
mendescamara requested review from iago1501 and vmourac-vtex and removed request for leo-prange-vtex May 6, 2026 17:49
@mendescamara
mendescamara merged commit ce0829b into master May 7, 2026
9 of 11 checks passed
@mendescamara
mendescamara deleted the feat/coerce-directive branch May 7, 2026 13:29
@vtex-io-ci-cd

vtex-io-ci-cd Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

Your PR has been merged! App is being published. 🚀
Version 2.175.1 → 2.175.2

After the publishing process has been completed (check #vtex-io-releases) and doing A/B tests with the new version, you can deploy your release by running:

vtex deploy vtex.store-graphql@2.175.2

After that your app will be updated on all accounts.

For more information on the deployment process check the docs. 📖

vsseixaso added a commit that referenced this pull request May 7, 2026
## What problem is this solving?

Builder `2.x` requires `@auth` on every query and mutation. The previous
upgrade attempt (#687) was reverted in #690 due to strict variable
coercion errors — **not** because of this requirement. That root cause
was fixed by the `@coerce` directive in #692, so the upgrade is safe to
land now.

## What changed?

- `manifest.json`: `"graphql": "1.x"` → `"graphql": "2.x"`
- `graphql/schema.graphql`: `@auth(scope: PUBLIC)` added to all queries
and mutations; `scalar Upload` declaration added

All fields use `scope: PUBLIC`, preserving the same public-by-default
behaviour as `1.x`.

## How to test it?

Run `./test-errors-regression.sh` against a linked workspace — all 14
regression cases should pass.

## Related

Re-introduces the GraphQL builder portion of #687.
Depends on the coercion fix from #692.

Made with Cursor

Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
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