From 2742fe72959bf30b012461ae7301d3a5f8dee9e9 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Sun, 22 Mar 2026 12:01:39 +0100 Subject: [PATCH 1/5] Stop calling typeLoader for built-in scalar names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Calling the typeLoader for built-in scalar names (String, Int, Float, Boolean, ID) breaks downstream consumers whose typeLoaders were not designed to handle these names. Remove typeLoader-based scalar override support, keeping only types-based overrides. Guard loadType() so the typeLoader is never invoked for built-in scalar names. Fixes https://github.com/webonyx/graphql-php/issues/1874 🤖 Generated with Claude Code --- CHANGELOG.md | 4 ++ benchmarks/ScalarOverrideBench.php | 27 -------- src/Type/Schema.php | 21 ++---- tests/Executor/ExecutorLazySchemaTest.php | 3 - tests/Type/ScalarOverridesTest.php | 83 +++++++++++++---------- 5 files changed, 55 insertions(+), 83 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea7ad93e3..592af4eb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ You can find and compare releases at the [GitHub release page](https://github.co ## Unreleased +### Changed + +- Stop calling `typeLoader` for built-in scalar names (`String`, `Int`, `Float`, `Boolean`, `ID`), use `types` config for per-schema scalar overrides https://github.com/webonyx/graphql-php/issues/1874 + ## v15.31.1 ### Fixed diff --git a/benchmarks/ScalarOverrideBench.php b/benchmarks/ScalarOverrideBench.php index e6e6c0b03..dcfcd620e 100644 --- a/benchmarks/ScalarOverrideBench.php +++ b/benchmarks/ScalarOverrideBench.php @@ -23,8 +23,6 @@ class ScalarOverrideBench { private Schema $schemaBaseline; - private Schema $schemaTypeLoader; - private Schema $schemaTypes; public function setUp(): void @@ -47,21 +45,6 @@ public function setUp(): void 'query' => $queryTypeBaseline, ]); - $queryTypeLoader = new ObjectType([ - 'name' => 'Query', - 'fields' => [ - 'greeting' => [ - 'type' => Type::string(), - 'resolve' => static fn (): string => 'hello world', - ], - ], - ]); - $typesForLoader = ['Query' => $queryTypeLoader, 'String' => $uppercaseString]; - $this->schemaTypeLoader = new Schema([ - 'query' => $queryTypeLoader, - 'typeLoader' => static fn (string $name): ?Type => $typesForLoader[$name] ?? null, - ]); - $queryTypeTypes = new ObjectType([ 'name' => 'Query', 'fields' => [ @@ -82,11 +65,6 @@ public function benchGetTypeWithoutOverride(): void $this->schemaBaseline->getType('String'); } - public function benchGetTypeWithTypeLoaderOverride(): void - { - $this->schemaTypeLoader->getType('String'); - } - public function benchGetTypeWithTypesOverride(): void { $this->schemaTypes->getType('String'); @@ -97,11 +75,6 @@ public function benchExecuteWithoutOverride(): void GraphQL::executeQuery($this->schemaBaseline, '{ greeting }'); } - public function benchExecuteWithTypeLoaderOverride(): void - { - GraphQL::executeQuery($this->schemaTypeLoader, '{ greeting }'); - } - public function benchExecuteWithTypesOverride(): void { GraphQL::executeQuery($this->schemaTypes, '{ greeting }'); diff --git a/src/Type/Schema.php b/src/Type/Schema.php index 8708552b4..a680cc28b 100644 --- a/src/Type/Schema.php +++ b/src/Type/Schema.php @@ -188,22 +188,6 @@ public function getTypeMap(): array $allReferencedTypes[$name] = $override; } - if (isset($this->config->typeLoader)) { - foreach (Type::BUILT_IN_SCALAR_NAMES as $scalarName) { - if (isset($scalarOverrides[$scalarName])) { - continue; - } - - $type = ($this->config->typeLoader)($scalarName); - if ($type instanceof ScalarType - && $type->name === $scalarName - && $type !== $builtInScalars[$scalarName] - ) { - $allReferencedTypes[$scalarName] = $type; - } - } - } - $this->resolvedTypes = $allReferencedTypes; $this->fullyLoaded = true; } @@ -366,6 +350,11 @@ private function loadType(string $typeName): ?Type return $this->getTypeMap()[$typeName] ?? null; } + // TODO https://github.com/webonyx/graphql-php/issues/1874 - reconsider supporting typeLoader-based scalar overrides in the next major version + if (isset(Type::builtInScalars()[$typeName])) { + return null; + } + $type = ($this->config->typeLoader)($typeName); if ($type === null) { return null; diff --git a/tests/Executor/ExecutorLazySchemaTest.php b/tests/Executor/ExecutorLazySchemaTest.php index 97f87bcb5..0ffedc6bb 100644 --- a/tests/Executor/ExecutorLazySchemaTest.php +++ b/tests/Executor/ExecutorLazySchemaTest.php @@ -213,7 +213,6 @@ public function testSimpleQuery(): void 'Query.fields', 'SomeObject', 'SomeObject.fields', - 'String', ]; self::assertSame($expected, $result->toArray(DebugFlag::INCLUDE_DEBUG_MESSAGE)); self::assertSame($expectedExecutorCalls, $this->calls); @@ -380,7 +379,6 @@ public function testDeepQuery(): void 'Query' => true, 'SomeObject' => true, 'OtherObject' => true, - 'String' => true, ], $this->loadedTypes ); @@ -389,7 +387,6 @@ public function testDeepQuery(): void 'Query.fields', 'SomeObject', 'SomeObject.fields', - 'String', ], $this->calls ); diff --git a/tests/Type/ScalarOverridesTest.php b/tests/Type/ScalarOverridesTest.php index ebee4ca88..8f0b21616 100644 --- a/tests/Type/ScalarOverridesTest.php +++ b/tests/Type/ScalarOverridesTest.php @@ -34,15 +34,13 @@ public function tearDown(): void Type::overrideStandardTypes(self::$originalStandardTypes); } - public function testTypeLoaderOverrideWorksEndToEnd(): void + public function testTypesOverrideWorksEndToEnd(): void { $uppercaseString = self::createUppercaseString(); - $queryType = self::createQueryType(); - $types = ['Query' => $queryType, 'String' => $uppercaseString]; $schema = new Schema([ - 'query' => $queryType, - 'typeLoader' => static fn (string $name): ?Type => $types[$name] ?? null, + 'query' => self::createQueryType(), + 'types' => [$uppercaseString], ]); $schema->assertValid(); @@ -52,19 +50,17 @@ public function testTypeLoaderOverrideWorksEndToEnd(): void self::assertSame(['data' => ['greeting' => 'HELLO WORLD']], $result->toArray()); } - public function testTypeLoaderOverrideWorksInProductionMode(): void + public function testTypesOverrideWorksInProductionMode(): void { $assertActive = (int) ini_get('assert.active'); @ini_set('assert.active', '0'); try { $uppercaseString = self::createUppercaseString(); - $queryType = self::createQueryType(); - $types = ['Query' => $queryType, 'String' => $uppercaseString]; $schema = new Schema([ - 'query' => $queryType, - 'typeLoader' => static fn (string $name): ?Type => $types[$name] ?? null, + 'query' => self::createQueryType(), + 'types' => [$uppercaseString], ]); $result = GraphQL::executeQuery($schema, '{ greeting }'); @@ -111,12 +107,10 @@ public function testTypesConfigOverrideWorksWithAssumeValid(): void public function testIntrospectionUsesOverriddenScalar(): void { $uppercaseString = self::createUppercaseString(); - $queryType = self::createQueryType(); - $types = ['Query' => $queryType, 'String' => $uppercaseString]; $schema = new Schema([ - 'query' => $queryType, - 'typeLoader' => static fn (string $name): ?Type => $types[$name] ?? null, + 'query' => self::createQueryType(), + 'types' => [$uppercaseString], ]); $result = GraphQL::executeQuery($schema, '{ __type(name: "Query") { fields { name } } }'); @@ -139,18 +133,14 @@ public function testTwoSchemasWithDifferentOverridesAreIndependent(): void 'serialize' => static fn ($value): string => strrev((string) $value), ]); - $queryTypeA = self::createQueryType(); - $typesA = ['Query' => $queryTypeA, 'String' => $uppercaseString]; $schemaA = new Schema([ - 'query' => $queryTypeA, - 'typeLoader' => static fn (string $name): ?Type => $typesA[$name] ?? null, + 'query' => self::createQueryType(), + 'types' => [$uppercaseString], ]); - $queryTypeB = self::createQueryType(); - $typesB = ['Query' => $queryTypeB, 'String' => $reversedString]; $schemaB = new Schema([ - 'query' => $queryTypeB, - 'typeLoader' => static fn (string $name): ?Type => $typesB[$name] ?? null, + 'query' => self::createQueryType(), + 'types' => [$reversedString], ]); $resultA = GraphQL::executeQuery($schemaA, '{ greeting }'); @@ -189,11 +179,9 @@ public function testNonOverriddenScalarsAreUnaffected(): void ], ]); - $types = ['Query' => $queryType, 'String' => $uppercaseString]; - $schema = new Schema([ 'query' => $queryType, - 'typeLoader' => static fn (string $name): ?Type => $types[$name] ?? null, + 'types' => [$uppercaseString], ]); $result = GraphQL::executeQuery($schema, '{ greeting count ratio active identifier }'); @@ -206,7 +194,7 @@ public function testNonOverriddenScalarsAreUnaffected(): void self::assertSame('abc-123', $data['identifier']); } - public function testTypeLoaderOverrideWithVariableOfOverriddenBuiltInScalarType(): void + public function testTypesOverrideWithVariableOfOverriddenBuiltInScalarType(): void { $customID = self::createCustomID(static fn ($value): string => (string) $value); @@ -223,11 +211,9 @@ public function testTypeLoaderOverrideWithVariableOfOverriddenBuiltInScalarType( ], ]); - $types = ['Query' => $queryType, 'ID' => $customID]; - $schema = new Schema([ 'query' => $queryType, - 'typeLoader' => static fn (string $name): ?Type => $types[$name] ?? null, + 'types' => [$customID], ]); $schema->assertValid(); @@ -238,7 +224,7 @@ public function testTypeLoaderOverrideWithVariableOfOverriddenBuiltInScalarType( self::assertSame(['data' => ['node' => 'node-abc-123']], $result->toArray()); } - public function testTypeLoaderOverrideWithNullableVariableOfOverriddenBuiltInScalarType(): void + public function testTypesOverrideWithNullableVariableOfOverriddenBuiltInScalarType(): void { $customString = self::createUppercaseString(); @@ -255,11 +241,9 @@ public function testTypeLoaderOverrideWithNullableVariableOfOverriddenBuiltInSca ], ]); - $types = ['Query' => $queryType, 'String' => $customString]; - $schema = new Schema([ 'query' => $queryType, - 'typeLoader' => static fn (string $name): ?Type => $types[$name] ?? null, + 'types' => [$customString], ]); $schema->assertValid(); @@ -270,7 +254,7 @@ public function testTypeLoaderOverrideWithNullableVariableOfOverriddenBuiltInSca self::assertSame(['data' => ['echo' => 'HELLO']], $result->toArray()); } - public function testTypeLoaderOverrideWithInputObjectFieldOfOverriddenBuiltInScalarType(): void + public function testTypesOverrideWithInputObjectFieldOfOverriddenBuiltInScalarType(): void { $customID = self::createCustomID(static fn ($value): string => 'custom-' . $value); @@ -295,11 +279,9 @@ public function testTypeLoaderOverrideWithInputObjectFieldOfOverriddenBuiltInSca ], ]); - $types = ['Query' => $queryType, 'ID' => $customID, 'NodeInput' => $inputType]; - $schema = new Schema([ 'query' => $queryType, - 'typeLoader' => static fn (string $name): ?Type => $types[$name] ?? null, + 'types' => [$customID], ]); $schema->assertValid(); @@ -316,6 +298,33 @@ public function testTypeLoaderOverrideWithInputObjectFieldOfOverriddenBuiltInSca self::assertSame(['data' => ['node' => 'custom-abc-123:test']], $result->toArray()); } + /** @see https://github.com/webonyx/graphql-php/issues/1874 */ + public function testTypeLoaderIsNotCalledForBuiltInScalarNames(): void + { + $calledWith = []; + $queryType = self::createQueryType(); + + $schema = new Schema([ + 'query' => $queryType, + 'typeLoader' => static function (string $name) use (&$calledWith, $queryType): ?Type { + $calledWith[] = $name; + + return match ($name) { + 'Query' => $queryType, + default => null, + }; + }, + ]); + + $result = GraphQL::executeQuery($schema, '{ greeting }'); + + self::assertSame(['data' => ['greeting' => 'hello world']], $result->toArray()); + + foreach (Type::BUILT_IN_SCALAR_NAMES as $scalarName) { + self::assertNotContains($scalarName, $calledWith, "typeLoader should not be called for built-in scalar '{$scalarName}'"); + } + } + /** @throws InvariantViolation */ private static function createCustomID(\Closure $parseValue): CustomScalarType { From 7235342bda88810f306e925979a364badac67624 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Sun, 22 Mar 2026 12:11:58 +0100 Subject: [PATCH 2/5] cheaper check --- src/Type/Definition/Type.php | 8 +++++++- src/Type/Schema.php | 8 +++++--- tests/Type/ScalarOverridesTest.php | 9 +++++---- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/Type/Definition/Type.php b/src/Type/Definition/Type.php index 8a9b05647..681799dbc 100644 --- a/src/Type/Definition/Type.php +++ b/src/Type/Definition/Type.php @@ -203,7 +203,7 @@ public static function overrideStandardTypes(array $types): void throw new InvariantViolation("Expecting instance of {$typeClass}, got {$notType}"); } - if (! in_array($type->name, self::BUILT_IN_SCALAR_NAMES, true)) { + if (! self::isBuiltInScalarName($type->name)) { $standardTypeNames = implode(', ', self::BUILT_IN_SCALAR_NAMES); $notStandardTypeName = Utils::printSafe($type->name); throw new InvariantViolation("Expecting one of the following names for a standard type: {$standardTypeNames}; got {$notStandardTypeName}"); @@ -231,6 +231,12 @@ public static function isBuiltInScalar($type): bool && in_array($type->name, self::BUILT_IN_SCALAR_NAMES, true); } + /** Checks if the given name is one of the built-in scalar type names (ID, String, Int, Float, Boolean). */ + public static function isBuiltInScalarName(string $name): bool + { + return in_array($name, self::BUILT_IN_SCALAR_NAMES, true); + } + /** * Determines if the given type is an input type. * diff --git a/src/Type/Schema.php b/src/Type/Schema.php index a680cc28b..e46a4b81e 100644 --- a/src/Type/Schema.php +++ b/src/Type/Schema.php @@ -346,16 +346,18 @@ public function hasType(string $name): bool */ private function loadType(string $typeName): ?Type { - if (! isset($this->config->typeLoader)) { + $typeLoader = $this->config->typeLoader; + + if (! isset($typeLoader)) { return $this->getTypeMap()[$typeName] ?? null; } // TODO https://github.com/webonyx/graphql-php/issues/1874 - reconsider supporting typeLoader-based scalar overrides in the next major version - if (isset(Type::builtInScalars()[$typeName])) { + if (Type::isBuiltInScalarName($typeName)) { return null; } - $type = ($this->config->typeLoader)($typeName); + $type = $typeLoader($typeName); if ($type === null) { return null; } diff --git a/tests/Type/ScalarOverridesTest.php b/tests/Type/ScalarOverridesTest.php index 8f0b21616..b26ae95d1 100644 --- a/tests/Type/ScalarOverridesTest.php +++ b/tests/Type/ScalarOverridesTest.php @@ -309,10 +309,11 @@ public function testTypeLoaderIsNotCalledForBuiltInScalarNames(): void 'typeLoader' => static function (string $name) use (&$calledWith, $queryType): ?Type { $calledWith[] = $name; - return match ($name) { - 'Query' => $queryType, - default => null, - }; + if ($name === 'Query') { + return $queryType; + } + + return null; }, ]); From 89cd752961326743f7564d7c312620af757bb352 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Sun, 22 Mar 2026 12:12:41 +0100 Subject: [PATCH 3/5] changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 592af4eb7..d3fbef97f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ You can find and compare releases at the [GitHub release page](https://github.co ### Changed -- Stop calling `typeLoader` for built-in scalar names (`String`, `Int`, `Float`, `Boolean`, `ID`), use `types` config for per-schema scalar overrides https://github.com/webonyx/graphql-php/issues/1874 +- Exclusively support `types` for per-schema scalar overrides, not `typeLoader` https://github.com/webonyx/graphql-php/pull/1884 ## v15.31.1 From 9125b75d4eb66ce0a0949ca9ef4d6aefbff43546 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Sun, 22 Mar 2026 15:12:29 +0100 Subject: [PATCH 4/5] Add test proving types override works alongside typeLoader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with Claude Code --- tests/Type/ScalarOverridesTest.php | 39 ++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/Type/ScalarOverridesTest.php b/tests/Type/ScalarOverridesTest.php index b26ae95d1..3e7a78512 100644 --- a/tests/Type/ScalarOverridesTest.php +++ b/tests/Type/ScalarOverridesTest.php @@ -298,6 +298,45 @@ public function testTypesOverrideWithInputObjectFieldOfOverriddenBuiltInScalarTy self::assertSame(['data' => ['node' => 'custom-abc-123:test']], $result->toArray()); } + public function testTypesOverrideWorksWithTypeLoader(): void + { + $uppercaseString = self::createUppercaseString(); + + $userType = new ObjectType([ + 'name' => 'User', + 'fields' => [ + 'name' => Type::string(), + ], + ]); + + $queryType = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'greeting' => [ + 'type' => Type::string(), + 'resolve' => static fn (): string => 'hello world', + ], + 'user' => [ + 'type' => $userType, + 'resolve' => static fn (): array => ['name' => 'Jane'], + ], + ], + ]); + + $types = ['Query' => $queryType, 'User' => $userType]; + $schema = new Schema([ + 'query' => $queryType, + 'typeLoader' => static fn (string $name): ?Type => $types[$name] ?? null, + 'types' => [$uppercaseString], + ]); + + $schema->assertValid(); + + $result = GraphQL::executeQuery($schema, '{ greeting user { name } }'); + + self::assertSame(['data' => ['greeting' => 'HELLO WORLD', 'user' => ['name' => 'JANE']]], $result->toArray()); + } + /** @see https://github.com/webonyx/graphql-php/issues/1874 */ public function testTypeLoaderIsNotCalledForBuiltInScalarNames(): void { From 5eab2406d631a00fef170ea838c0892d65c63b58 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Sun, 22 Mar 2026 15:15:00 +0100 Subject: [PATCH 5/5] Add test for types override with typeLoader and document per-schema scalar overrides MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with Claude Code --- docs/schema-definition.md | 16 ++++++++-------- docs/type-definitions/scalars.md | 27 +++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/docs/schema-definition.md b/docs/schema-definition.md index 35b7c2b70..5c0ffe1d9 100644 --- a/docs/schema-definition.md +++ b/docs/schema-definition.md @@ -78,14 +78,14 @@ with complex input values (see [Mutations and Input Types](type-definitions/inpu The schema constructor expects an instance of [`GraphQL\Type\SchemaConfig`](class-reference.md#graphqltypeschemaconfig) or an array with the following options: -| Option | Type | Notes | -| ------------ | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| query | `ObjectType` or `callable(): ?ObjectType` or `null` | **Required.** Object type (usually named `Query`) containing root-level fields of your read API | -| mutation | `ObjectType` or `callable(): ?ObjectType` or `null` | Object type (usually named `Mutation`) containing root-level fields of your write API | -| subscription | `ObjectType` or `callable(): ?ObjectType` or `null` | Reserved for future subscriptions implementation. Currently presented for compatibility with introspection query of **graphql-js**, used by various clients (like Relay or GraphiQL) | -| directives | `array` | A full list of [directives](type-definitions/directives.md) supported by your schema. By default, contains built-in **@skip** and **@include** directives.

If you pass your own directives and still want to use built-in directives - add them explicitly. For example:

_array_merge(GraphQL::getStandardDirectives(), [$myCustomDirective]);_ | -| types | `array` | List of object types which cannot be detected by **graphql-php** during static schema analysis.

Most often this happens when the object type is never referenced in fields directly but is still a part of a schema because it implements an interface which resolves to this object type in its **resolveType** callable.

Note that you are not required to pass all of your types here - it is simply a workaround for a concrete use-case. | -| typeLoader | `callable(string $name): Type` | Expected to return a type instance given the name. Must always return the same instance if called multiple times, see [lazy loading](#lazy-loading-of-types). See section below on lazy type loading. | +| Option | Type | Notes | +| ------------ | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| query | `ObjectType` or `callable(): ?ObjectType` or `null` | **Required.** Object type (usually named `Query`) containing root-level fields of your read API | +| mutation | `ObjectType` or `callable(): ?ObjectType` or `null` | Object type (usually named `Mutation`) containing root-level fields of your write API | +| subscription | `ObjectType` or `callable(): ?ObjectType` or `null` | Reserved for future subscriptions implementation. Currently presented for compatibility with introspection query of **graphql-js**, used by various clients (like Relay or GraphiQL) | +| directives | `array` | A full list of [directives](type-definitions/directives.md) supported by your schema. By default, contains built-in **@skip** and **@include** directives.

If you pass your own directives and still want to use built-in directives - add them explicitly. For example:

_array_merge(GraphQL::getStandardDirectives(), [$myCustomDirective]);_ | +| types | `iterable` | Additional types to register in the schema.

Use this for object types that are not directly referenced in fields but implement an interface that resolves to them via **resolveType**.

Can also contain custom scalar types named like built-in scalars (`String`, `Int`, etc.) to [override them](type-definitions/scalars.md#overriding-built-in-scalars) on a per-schema basis. | +| typeLoader | `callable(string $name): Type` | Expected to return a type instance given the name. Must always return the same instance if called multiple times, see [lazy loading](#lazy-loading-of-types). See section below on lazy type loading. | ### Using config class diff --git a/docs/type-definitions/scalars.md b/docs/type-definitions/scalars.md index 9601710ee..09cc82a35 100644 --- a/docs/type-definitions/scalars.md +++ b/docs/type-definitions/scalars.md @@ -106,3 +106,30 @@ $emailType = new CustomScalarType([ Keep in mind the passed functions will be called statically, so a passed in `callable` such as `[Foo::class, 'bar']` should only reference static class methods. + +## Overriding Built-in Scalars + +You can override built-in scalars (`String`, `Int`, `Float`, `Boolean`, `ID`) on a per-schema basis by passing a custom scalar with the same name through the `types` option. +This works with or without a `typeLoader`: + +```php +use GraphQL\Type\Definition\CustomScalarType; +use GraphQL\Type\Definition\Type; +use GraphQL\Type\Schema; + +$uppercaseString = new CustomScalarType([ + 'name' => 'String', + 'serialize' => static fn ($value): string => strtoupper((string) $value), +]); + +$schema = new Schema([ + 'query' => $queryType, + 'typeLoader' => $myTypeLoader, + 'types' => [$uppercaseString], +]); +``` + +The custom scalar replaces the built-in one throughout the entire schema, affecting both serialization of results and coercion of inputs. + +> **Note:** The `typeLoader` is never called for built-in scalar names. +> Always use `types` to override them.