diff --git a/docs-website/docs.json b/docs-website/docs.json
index 9bf12830d..f31810c91 100644
--- a/docs-website/docs.json
+++ b/docs-website/docs.json
@@ -291,6 +291,7 @@
]
},
"router/relaxed-field-selection-merging-nullability",
+ "router/string-literals-for-enums",
"router/file-upload",
"router/access-logs",
"router/profiling",
diff --git a/docs-website/router/configuration.mdx b/docs-website/router/configuration.mdx
index c0c3bd88e..ebfc74831 100644
--- a/docs-website/router/configuration.mdx
+++ b/docs-website/router/configuration.mdx
@@ -2051,6 +2051,7 @@ Configure the GraphQL Execution Engine of the Router.
| ENGINE_SUBSCRIPTION_FETCH_TIMEOUT | subscription_fetch_timeout | | The maximum time a subscription fetch can take before it is considered timed out. | 30s |
| ENGINE_ENABLE_DEFER | enable_defer | | Enables support for the `@defer` directive, allowing clients to defer parts of a query so that the initial response is returned faster and deferred fields are streamed incrementally. | false |
| ENGINE_RELAX_SUBGRAPH_OPERATION_FIELD_SELECTION_MERGING_NULLABILITY | relax_subgraph_operation_field_selection_merging_nullability | | Relaxes nullability validation for [field selection merging](/router/relaxed-field-selection-merging-nullability) across union member types. | false |
+| ENGINE_ALLOW_STRING_LITERALS_FOR_ENUMS | allow_string_literals_for_enums | | Accepts a [string literal where an enum value is expected](/router/string-literals-for-enums) if the string content matches one of the enum's values. This is a deviation from the GraphQL specification. | false |
**Subject to change.** `enable_net_poll`, `websocket_server_poll_timeout`, and `websocket_server_conn_buffer_size` may be removed or changed in a future release without a standard deprecation cycle. Avoid depending on them.
@@ -2098,6 +2099,7 @@ engine:
enable_subgraph_fetch_operation_name: true
subscription_fetch_timeout: 30s
relax_subgraph_operation_field_selection_merging_nullability: false
+ allow_string_literals_for_enums: false
```
### Debug Configuration
diff --git a/docs-website/router/string-literals-for-enums.mdx b/docs-website/router/string-literals-for-enums.mdx
new file mode 100644
index 000000000..8c11c1259
--- /dev/null
+++ b/docs-website/router/string-literals-for-enums.mdx
@@ -0,0 +1,71 @@
+---
+title: "String Literals for Enums"
+icon: "quotes"
+description: "Accept string literals for enum values as an opt-in deviation from the GraphQL specification."
+---
+
+The GraphQL specification distinguishes between enum literals and string literals in an operation document. An enum argument accepts the enum literal `VALUE1`, but not the string literal `"VALUE1"`. By default, the router rejects a string literal used for an enum type with a validation error:
+
+```
+Enum "SomeEnum" cannot represent non-enum value: "VALUE1".
+```
+
+The `allow_string_literals_for_enums` option accepts such string literals when the string content matches one of the enum's values. Strings that do not match a value are still rejected:
+
+```
+Value "NOPE" does not exist in "SomeEnum" enum.
+```
+
+
+ This is a deviation from the GraphQL specification. Servers built on graphql-js reject string literals for enums. Prefer fixing clients to send enum literals. Use this option to keep existing clients working while they migrate.
+
+
+## Examples
+
+Given the schema:
+
+```graphql
+enum SomeEnum {
+ VALUE1
+ VALUE2
+}
+
+type Query {
+ field(arg: SomeEnum): String
+}
+```
+
+The following queries behave as listed:
+
+```graphql
+{ field(arg: VALUE1) } # Valid. Enum literal.
+{ field(arg: "VALUE1") } # Invalid by default. Valid with this option enabled.
+{ field(arg: "NOPE") } # Invalid. "NOPE" is not a value of SomeEnum.
+```
+
+Variables are not affected by this option. JSON has no enum type, so a string is the standard representation for an enum variable value and is always accepted when it matches an enum value:
+
+```graphql
+query ($arg: SomeEnum) { field(arg: $arg) }
+# variables: {"arg": "VALUE1"} is valid with or without this option
+```
+
+## Configuration
+
+
+
+ ```bash
+ ENGINE_ALLOW_STRING_LITERALS_FOR_ENUMS=true
+ ```
+
+
+ ```yaml config.yaml
+ version: "1"
+
+ engine:
+ allow_string_literals_for_enums: true
+ ```
+
+
+
+This option is disabled by default. See the [Router Configuration reference](/router/configuration#router-engine-configuration) for all engine flags.
diff --git a/router-tests/operations/inline_literal_validation_test.go b/router-tests/operations/inline_literal_validation_test.go
index ac10abf0c..18aa631fb 100644
--- a/router-tests/operations/inline_literal_validation_test.go
+++ b/router-tests/operations/inline_literal_validation_test.go
@@ -204,6 +204,76 @@ func TestInlineLiteralValidationWithPersistedOperations(t *testing.T) {
})
}
+// TestAllowStringLiteralsForEnums verifies the opt-in spec deviation
+// (engine execution config allow_string_literals_for_enums) that accepts a string
+// literal where an enum value is expected, as long as the string content matches
+// one of the enum's values. Without the flag, the router enforces the strict
+// GraphQL spec behavior and rejects such literals at validation.
+func TestAllowStringLiteralsForEnums(t *testing.T) {
+ t.Parallel()
+
+ t.Run("disabled by default", func(t *testing.T) {
+ testenv.Run(t, &testenv.Config{}, func(t *testing.T, xEnv *testenv.Environment) {
+ t.Run("rejects string literal for enum argument", func(t *testing.T) {
+ res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
+ Query: `query { rootFieldWithListOfEnumArg(arg: ["A"]) }`,
+ })
+ requireValidationError(t, res.Body, `Enum "EnumType" cannot represent non-enum value: "A".`)
+ })
+
+ t.Run("rejects string literal for enum field of input object", func(t *testing.T) {
+ res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
+ Query: `query { rootFieldWithInput(arg: {enum: "A"}) }`,
+ })
+ requireValidationError(t, res.Body, `Enum "EnumType" cannot represent non-enum value: "A".`)
+ })
+ })
+ })
+
+ t.Run("enabled", func(t *testing.T) {
+ testenv.Run(t, &testenv.Config{
+ ModifyEngineExecutionConfiguration: func(cfg *config.EngineExecutionConfiguration) {
+ cfg.AllowStringLiteralsForEnums = true
+ },
+ }, func(t *testing.T, xEnv *testenv.Environment) {
+ t.Run("accepts string literal matching an enum value for enum argument", func(t *testing.T) {
+ res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
+ Query: `query { rootFieldWithListOfEnumArg(arg: ["A"]) }`,
+ })
+ assert.Equal(t, `{"data":{"rootFieldWithListOfEnumArg":["A"]}}`, res.Body)
+ })
+
+ t.Run("accepts string literal matching an enum value for enum field of input object", func(t *testing.T) {
+ res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
+ Query: `query { rootFieldWithInput(arg: {enum: "A"}) }`,
+ })
+ assert.Equal(t, `{"data":{"rootFieldWithInput":"A"}}`, res.Body)
+ })
+
+ t.Run("rejects string literal not matching an enum value", func(t *testing.T) {
+ res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
+ Query: `query { rootFieldWithListOfEnumArg(arg: ["NOPE"]) }`,
+ })
+ requireValidationError(t, res.Body, `Value "NOPE" does not exist in "EnumType" enum.`)
+ })
+
+ t.Run("still accepts inline enum literal for enum argument", func(t *testing.T) {
+ res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
+ Query: `query { rootFieldWithListOfEnumArg(arg: [A]) }`,
+ })
+ assert.Equal(t, `{"data":{"rootFieldWithListOfEnumArg":["A"]}}`, res.Body)
+ })
+
+ t.Run("still rejects string literal for a non-enum argument mismatch", func(t *testing.T) {
+ res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
+ Query: `query { floatField(arg: "1.5") }`,
+ })
+ requireValidationError(t, res.Body, `Float cannot represent non numeric value: "1.5"`)
+ })
+ })
+ })
+}
+
// requireValidationError asserts the GraphQL response body carries exactly one
// error with the given message and no data payload.
//
diff --git a/router/core/graph_server.go b/router/core/graph_server.go
index 8c5182ce4..d5231277a 100644
--- a/router/core/graph_server.go
+++ b/router/core/graph_server.go
@@ -1599,10 +1599,11 @@ func (s *graphServer) buildGraphMux(
ApolloRouterCompatibilityFlags: s.apolloRouterCompatibilityFlags,
DisableExposingVariablesContentOnValidationError: s.engineExecutionConfiguration.DisableExposingVariablesContentOnValidationError,
RelaxSubgraphOperationFieldSelectionMergingNullability: s.engineExecutionConfiguration.RelaxSubgraphOperationFieldSelectionMergingNullability,
- EnableDefer: s.engineExecutionConfiguration.EnableDefer,
- ComplexityLimits: s.securityConfiguration.ComplexityLimits,
- CostControl: s.securityConfiguration.CostControl,
- ValidateInlineArguments: s.engineExecutionConfiguration.ValidateInlineArguments,
+ AllowStringLiteralsForEnums: s.engineExecutionConfiguration.AllowStringLiteralsForEnums,
+ EnableDefer: s.engineExecutionConfiguration.EnableDefer,
+ ComplexityLimits: s.securityConfiguration.ComplexityLimits,
+ CostControl: s.securityConfiguration.CostControl,
+ ValidateInlineArguments: s.engineExecutionConfiguration.ValidateInlineArguments,
})
if opts.ReloadPersistentState.inMemoryPlanCacheFallback.IsEnabled() {
diff --git a/router/core/operation_processor.go b/router/core/operation_processor.go
index 656abdd66..6bbad944b 100644
--- a/router/core/operation_processor.go
+++ b/router/core/operation_processor.go
@@ -128,6 +128,7 @@ type OperationProcessorOptions struct {
ApolloRouterCompatibilityFlags config.ApolloRouterCompatibilityFlags
DisableExposingVariablesContentOnValidationError bool
RelaxSubgraphOperationFieldSelectionMergingNullability bool
+ AllowStringLiteralsForEnums bool
ComplexityLimits *config.ComplexityLimits
CostControl *config.CostControl
ParserTokenizerLimits astparser.TokenizerLimits
@@ -1563,6 +1564,7 @@ type parseKitOptions struct {
apolloRouterCompatibilityFlags config.ApolloRouterCompatibilityFlags
disableExposingVariablesContentOnValidationError bool
relaxSubgraphOperationFieldSelectionMergingNullability bool
+ allowStringLiteralsForEnums bool
enableDefer bool
validateInlineArguments config.ValidateInlineArguments
}
@@ -1638,6 +1640,9 @@ func createOperationValidator(options *parseKitOptions) *astvalidation.Operation
if options.relaxSubgraphOperationFieldSelectionMergingNullability {
opts = append(opts, astvalidation.WithRelaxFieldSelectionMergingNullability())
}
+ if options.allowStringLiteralsForEnums {
+ opts = append(opts, astvalidation.WithAllowStringLiteralsForEnums())
+ }
return astvalidation.DefaultOperationValidator(opts...)
}
@@ -1662,6 +1667,7 @@ func NewOperationProcessor(opts OperationProcessorOptions) *OperationProcessor {
apolloRouterCompatibilityFlags: opts.ApolloRouterCompatibilityFlags,
disableExposingVariablesContentOnValidationError: opts.DisableExposingVariablesContentOnValidationError,
relaxSubgraphOperationFieldSelectionMergingNullability: opts.RelaxSubgraphOperationFieldSelectionMergingNullability,
+ allowStringLiteralsForEnums: opts.AllowStringLiteralsForEnums,
validateInlineArguments: opts.ValidateInlineArguments,
},
}
diff --git a/router/pkg/config/config.go b/router/pkg/config/config.go
index d06034474..1d12fff4c 100644
--- a/router/pkg/config/config.go
+++ b/router/pkg/config/config.go
@@ -517,6 +517,8 @@ type EngineExecutionConfiguration struct {
RelaxSubgraphOperationFieldSelectionMergingNullability bool `envDefault:"false" env:"ENGINE_RELAX_SUBGRAPH_OPERATION_FIELD_SELECTION_MERGING_NULLABILITY" yaml:"relax_subgraph_operation_field_selection_merging_nullability"`
+ AllowStringLiteralsForEnums bool `envDefault:"false" env:"ENGINE_ALLOW_STRING_LITERALS_FOR_ENUMS" yaml:"allow_string_literals_for_enums"`
+
ValidateInlineArguments ValidateInlineArguments `yaml:"validate_inline_arguments" envPrefix:"ENGINE_VALIDATE_INLINE_ARGUMENTS_"`
}
diff --git a/router/pkg/config/config.schema.json b/router/pkg/config/config.schema.json
index bad726c88..7a9ca9ab9 100644
--- a/router/pkg/config/config.schema.json
+++ b/router/pkg/config/config.schema.json
@@ -4093,6 +4093,11 @@
"type": "boolean",
"default": false,
"description": "Relaxes nullability validation for field selection merging when enclosing types are non-overlapping concrete object types. When enabled, fields with differing nullability (e.g. String! vs String) in inline fragments on different union member types will not cause validation errors."
+ },
+ "allow_string_literals_for_enums": {
+ "type": "boolean",
+ "default": false,
+ "description": "Enables a deliberate deviation from the GraphQL specification that accepts a string literal where an enum value is expected, as long as the string content matches one of the enum's values (e.g. f(arg: \"VALUE1\") for enum SomeEnum { VALUE1 }). When disabled, only enum literals are valid inline values for enum types."
}
}
},
diff --git a/router/pkg/config/testdata/config_defaults.json b/router/pkg/config/testdata/config_defaults.json
index e46a61389..4c19bfa1e 100644
--- a/router/pkg/config/testdata/config_defaults.json
+++ b/router/pkg/config/testdata/config_defaults.json
@@ -517,6 +517,7 @@
"WebSocketClientAckTimeout": 30000000000,
"ValidateRequiredExternalFields": false,
"RelaxSubgraphOperationFieldSelectionMergingNullability": false,
+ "AllowStringLiteralsForEnums": false,
"ValidateInlineArguments": {
"Mode": "off",
"EnforceHTTPStatusCode": 400,
diff --git a/router/pkg/config/testdata/config_full.json b/router/pkg/config/testdata/config_full.json
index 2661afcea..f10f01d2e 100644
--- a/router/pkg/config/testdata/config_full.json
+++ b/router/pkg/config/testdata/config_full.json
@@ -985,6 +985,7 @@
"WebSocketClientAckTimeout": 30000000000,
"ValidateRequiredExternalFields": false,
"RelaxSubgraphOperationFieldSelectionMergingNullability": false,
+ "AllowStringLiteralsForEnums": false,
"ValidateInlineArguments": {
"Mode": "off",
"EnforceHTTPStatusCode": 400,