From aebdab41624e06cbad20b8d281706f9c7c285930 Mon Sep 17 00:00:00 2001
From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com>
Date: Wed, 6 May 2026 14:31:03 +0200
Subject: [PATCH 1/2] Add DisableInternalDirectives
---
.../Core/src/Types/IReadOnlySchemaOptions.cs | 7 +++++++
src/HotChocolate/Core/src/Types/SchemaOptions.cs | 4 ++++
.../Types/Types/DirectiveType.Initialization.cs | 3 ++-
.../v16/migrating/migrate-from-15-to-16.md | 14 ++++++++++++++
4 files changed, 27 insertions(+), 1 deletion(-)
diff --git a/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs b/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs
index bb3678b4e98..3fd38189f9c 100644
--- a/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs
+++ b/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs
@@ -85,6 +85,13 @@ public interface IReadOnlySchemaOptions
///
DirectiveVisibility DefaultDirectiveVisibility { get; }
+ ///
+ /// Disables the concept of internal directives so that all directives are treated as public.
+ /// When set to true, this overrides any explicit Internal() calls on
+ /// directive types and the setting.
+ ///
+ bool DisableInternalDirectives { get; }
+
///
/// Defines that the default resolver execution strategy.
///
diff --git a/src/HotChocolate/Core/src/Types/SchemaOptions.cs b/src/HotChocolate/Core/src/Types/SchemaOptions.cs
index ee7ef7f294c..f48e9f168f2 100644
--- a/src/HotChocolate/Core/src/Types/SchemaOptions.cs
+++ b/src/HotChocolate/Core/src/Types/SchemaOptions.cs
@@ -81,6 +81,9 @@ public FieldBindingFlags DefaultFieldBindingFlags
public DirectiveVisibility DefaultDirectiveVisibility { get; set; } =
DirectiveVisibility.Public;
+ ///
+ public bool DisableInternalDirectives { get; set; }
+
///
public ExecutionStrategy DefaultResolverStrategy { get; set; } =
ExecutionStrategy.Parallel;
@@ -207,6 +210,7 @@ public static SchemaOptions FromOptions(IReadOnlySchemaOptions options)
DefaultBindingBehavior = options.DefaultBindingBehavior,
EnableDirectiveIntrospection = options.EnableDirectiveIntrospection,
DefaultDirectiveVisibility = options.DefaultDirectiveVisibility,
+ DisableInternalDirectives = options.DisableInternalDirectives,
DefaultResolverStrategy = options.DefaultResolverStrategy,
ValidatePipelineOrder = options.ValidatePipelineOrder,
StrictRuntimeTypeValidation = options.StrictRuntimeTypeValidation,
diff --git a/src/HotChocolate/Core/src/Types/Types/DirectiveType.Initialization.cs b/src/HotChocolate/Core/src/Types/Types/DirectiveType.Initialization.cs
index 9c265cc0f7f..3d07d5be9bc 100644
--- a/src/HotChocolate/Core/src/Types/Types/DirectiveType.Initialization.cs
+++ b/src/HotChocolate/Core/src/Types/Types/DirectiveType.Initialization.cs
@@ -68,7 +68,8 @@ protected override void OnCompleteType(
Locations = configuration.Locations;
Arguments = OnCompleteFields(context, configuration);
- IsPublic = configuration.IsPublic;
+ IsPublic = configuration.IsPublic
+ || context.DescriptorContext.Options.DisableInternalDirectives;
Middleware = OnCompleteMiddleware(context, configuration);
_createInstance = OnCompleteCreateInstance(context, configuration);
diff --git a/website/src/docs/hotchocolate/v16/migrating/migrate-from-15-to-16.md b/website/src/docs/hotchocolate/v16/migrating/migrate-from-15-to-16.md
index 3428adfa99a..6e47c90149c 100644
--- a/website/src/docs/hotchocolate/v16/migrating/migrate-from-15-to-16.md
+++ b/website/src/docs/hotchocolate/v16/migrating/migrate-from-15-to-16.md
@@ -79,6 +79,20 @@ If you need to access the application service provider from within the schema se
IServiceProvider applicationServices = schemaServices.GetRootServiceProvider();
```
+## Internal directives hidden from schema endpoint
+
+Previously, the `/graphql/schema.graphql` endpoint was returning the schema containing internal directives like `@authorize`. Starting with v16 the endpoint no longer includes internal directives by default.
+
+If you need to retain the previous behavior, set `DisableInternalDirectives` to `true` through `ModifyOptions`. This treats every directive as public, even directives that explicitly call `Internal()` and regardless of `DefaultDirectiveVisibility`:
+
+```csharp
+builder.Services
+ .AddGraphQLServer()
+ .ModifyOptions(o => o.DisableInternalDirectives = true);
+```
+
+Be aware that internal directives may carry sensitive information (for example, authorization policies attached via `@authorize`). Only enable this if you understand and accept that risk.
+
## Cache size configuration
Previously, document and operation cache sizes were globally configured through the `IServiceCollection`. In an effort to align and properly scope our configuration APIs, we've moved the configuration of these caches to the `IRequestExecutorBuilder`. If you're currently calling `AddDocumentCache` or `AddOperationCache` directly on the `IServiceCollection`, move the configuration to `ModifyOptions` on the `IRequestExecutorBuilder`:
From 92144d4b885ae9b4630215ff62e1cdde169370ff Mon Sep 17 00:00:00 2001
From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com>
Date: Wed, 6 May 2026 14:40:50 +0200
Subject: [PATCH 2/2] Add tests
---
.../HttpGetSchemaMiddlewareTests.cs | 26 ++
...GetSemanticNonNullSchemaMiddlewareTests.cs | 26 ++
...hen_DisableInternalDirectives_Is_True.snap | 22 ++
...hen_DisableInternalDirectives_Is_True.snap | 24 ++
.../Execution.Tests/IntrospectionTests.cs | 54 ++++
...hen_DisableInternalDirectives_Is_True.snap | 271 ++++++++++++++++++
6 files changed, 423 insertions(+)
create mode 100644 src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_Schema_Includes_Internal_Directives_When_DisableInternalDirectives_Is_True.snap
create mode 100644 src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSemanticNonNullSchemaMiddlewareTests.Download_GraphQL_SemanticNonNull_Schema_Includes_Internal_Directives_When_DisableInternalDirectives_Is_True.snap
create mode 100644 src/HotChocolate/Core/test/Execution.Tests/__snapshots__/IntrospectionTests.DirectiveIntrospection_AllDirectives_Public_When_DisableInternalDirectives_Is_True.snap
diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpGetSchemaMiddlewareTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpGetSchemaMiddlewareTests.cs
index 9514b9852a3..0d5c6480b08 100644
--- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpGetSchemaMiddlewareTests.cs
+++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpGetSchemaMiddlewareTests.cs
@@ -281,6 +281,32 @@ public async Task Download_GraphQL_Schema_Does_Not_Include_Internal_Directives()
result.MatchSnapshot();
}
+ [Fact]
+ public async Task Download_GraphQL_Schema_Includes_Internal_Directives_When_DisableInternalDirectives_Is_True()
+ {
+ // arrange
+ var server = ServerFactory.Create(
+ services => services
+ .AddRouting()
+ .AddGraphQLServer()
+ .ModifyOptions(o => o.DisableInternalDirectives = true)
+ .AddDirectiveType()
+ .AddQueryType(),
+ app => app
+ .UseRouting()
+ .UseEndpoints(endpoints => endpoints.MapGraphQLSchema()));
+ var url = TestServerExtensions.CreateUrl("/graphql/sdl");
+ var request = new HttpRequestMessage(HttpMethod.Get, url);
+
+ // act
+ var response = await server.CreateClient().SendAsync(request);
+
+ // assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var result = await response.Content.ReadAsStringAsync();
+ result.MatchSnapshot();
+ }
+
private sealed class StaticTimeProvider : ITimeProvider
{
public DateTimeOffset UtcNow { get; } = new(2021, 1, 1, 0, 0, 0, TimeSpan.Zero);
diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpGetSemanticNonNullSchemaMiddlewareTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpGetSemanticNonNullSchemaMiddlewareTests.cs
index b3d33f8257b..97a96c6aadf 100644
--- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpGetSemanticNonNullSchemaMiddlewareTests.cs
+++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpGetSemanticNonNullSchemaMiddlewareTests.cs
@@ -133,6 +133,32 @@ public async Task Download_GraphQL_SemanticNonNull_Schema_Does_Not_Include_Inter
result.MatchSnapshot();
}
+ [Fact]
+ public async Task Download_GraphQL_SemanticNonNull_Schema_Includes_Internal_Directives_When_DisableInternalDirectives_Is_True()
+ {
+ // arrange
+ var server = ServerFactory.Create(
+ services => services
+ .AddRouting()
+ .AddGraphQLServer()
+ .ModifyOptions(o => o.DisableInternalDirectives = true)
+ .AddDirectiveType()
+ .AddQueryType(),
+ app => app
+ .UseRouting()
+ .UseEndpoints(endpoints => endpoints.MapGraphQLSemanticNonNullSchema()));
+ var url = TestServerExtensions.CreateUrl("/graphql/semantic-non-null-schema.graphql");
+ var request = new HttpRequestMessage(HttpMethod.Get, url);
+
+ // act
+ var response = await server.CreateClient().SendAsync(request);
+
+ // assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var result = await response.Content.ReadAsStringAsync();
+ result.MatchSnapshot();
+ }
+
private sealed class StaticTimeProvider : ITimeProvider
{
public DateTimeOffset UtcNow { get; } = new(2021, 1, 1, 0, 0, 0, TimeSpan.Zero);
diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_Schema_Includes_Internal_Directives_When_DisableInternalDirectives_Is_True.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_Schema_Includes_Internal_Directives_When_DisableInternalDirectives_Is_True.snap
new file mode 100644
index 00000000000..3cc948a2566
--- /dev/null
+++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_Schema_Includes_Internal_Directives_When_DisableInternalDirectives_Is_True.snap
@@ -0,0 +1,22 @@
+schema {
+ query: Query
+}
+
+type Query {
+ secret: String! @internal @cost(weight: "10")
+ public: String! @cost(weight: "10")
+}
+
+"The purpose of the `cost` directive is to define a `weight` for GraphQL types, fields, and arguments. Static analysis can use these weights when calculating the overall cost of a query or response."
+directive @cost(
+ "The `weight` argument defines what value to add to the overall cost for every appearance, or possible appearance, of a type, field, argument, etc."
+ weight: String!
+) on
+ | SCALAR
+ | OBJECT
+ | FIELD_DEFINITION
+ | ARGUMENT_DEFINITION
+ | ENUM
+ | INPUT_FIELD_DEFINITION
+
+directive @internal on FIELD_DEFINITION
diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSemanticNonNullSchemaMiddlewareTests.Download_GraphQL_SemanticNonNull_Schema_Includes_Internal_Directives_When_DisableInternalDirectives_Is_True.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSemanticNonNullSchemaMiddlewareTests.Download_GraphQL_SemanticNonNull_Schema_Includes_Internal_Directives_When_DisableInternalDirectives_Is_True.snap
new file mode 100644
index 00000000000..4b931794efd
--- /dev/null
+++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSemanticNonNullSchemaMiddlewareTests.Download_GraphQL_SemanticNonNull_Schema_Includes_Internal_Directives_When_DisableInternalDirectives_Is_True.snap
@@ -0,0 +1,24 @@
+schema {
+ query: Query
+}
+
+type Query {
+ secret: String @internal @cost(weight: "10") @semanticNonNull
+ public: String @cost(weight: "10") @semanticNonNull
+}
+
+"The purpose of the `cost` directive is to define a `weight` for GraphQL types, fields, and arguments. Static analysis can use these weights when calculating the overall cost of a query or response."
+directive @cost(
+ "The `weight` argument defines what value to add to the overall cost for every appearance, or possible appearance, of a type, field, argument, etc."
+ weight: String!
+) on
+ | SCALAR
+ | OBJECT
+ | FIELD_DEFINITION
+ | ARGUMENT_DEFINITION
+ | ENUM
+ | INPUT_FIELD_DEFINITION
+
+directive @internal on FIELD_DEFINITION
+
+directive @semanticNonNull(levels: [Int!] = [0]) on FIELD_DEFINITION
diff --git a/src/HotChocolate/Core/test/Execution.Tests/IntrospectionTests.cs b/src/HotChocolate/Core/test/Execution.Tests/IntrospectionTests.cs
index 26fbcc00e1c..7376259e088 100644
--- a/src/HotChocolate/Core/test/Execution.Tests/IntrospectionTests.cs
+++ b/src/HotChocolate/Core/test/Execution.Tests/IntrospectionTests.cs
@@ -356,6 +356,60 @@ directive @bar(baz: String, quox: SomeInput) repeatable on FIELD_DEFINITION
result.MatchSnapshot();
}
+ [Fact]
+ public async Task DirectiveIntrospection_AllDirectives_Public_When_DisableInternalDirectives_Is_True()
+ {
+ var result =
+ await new ServiceCollection()
+ .AddGraphQL()
+ .AddDocumentFromString(
+ """
+ type Query {
+ foo: String
+ @foo
+ @bar(baz: "ABC")
+ @bar(baz: null)
+ @bar(quox: { a: "ABC" })
+ @bar(quox: { a: "DEF" })
+ @bar
+ }
+
+ input SomeInput {
+ a: String!
+ }
+
+ directive @bar(baz: String, quox: SomeInput) repeatable on FIELD_DEFINITION
+ """)
+ .UseField(next => next)
+ .ModifyOptions(o => o.EnableDirectiveIntrospection = true)
+ .ModifyOptions(o => o.DefaultDirectiveVisibility = DirectiveVisibility.Internal)
+ .ModifyOptions(o => o.DisableInternalDirectives = true)
+ .AddDirectiveType(new DirectiveType(d =>
+ {
+ d.Name("foo");
+ d.Location(DirectiveLocation.FieldDefinition);
+ d.Internal();
+ }))
+ .ExecuteRequestAsync(
+ @"{
+ __schema {
+ types {
+ fields {
+ appliedDirectives {
+ name
+ args {
+ name
+ value
+ }
+ }
+ }
+ }
+ }
+ }");
+
+ result.MatchSnapshot();
+ }
+
private static Schema CreateSchema()
{
return SchemaBuilder.New()
diff --git a/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/IntrospectionTests.DirectiveIntrospection_AllDirectives_Public_When_DisableInternalDirectives_Is_True.snap b/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/IntrospectionTests.DirectiveIntrospection_AllDirectives_Public_When_DisableInternalDirectives_Is_True.snap
new file mode 100644
index 00000000000..b3c26b368b0
--- /dev/null
+++ b/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/IntrospectionTests.DirectiveIntrospection_AllDirectives_Public_When_DisableInternalDirectives_Is_True.snap
@@ -0,0 +1,271 @@
+{
+ "data": {
+ "__schema": {
+ "types": [
+ {
+ "fields": [
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ }
+ ]
+ },
+ {
+ "fields": null
+ },
+ {
+ "fields": [
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ }
+ ]
+ },
+ {
+ "fields": [
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ }
+ ]
+ },
+ {
+ "fields": [
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ }
+ ]
+ },
+ {
+ "fields": [
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ }
+ ]
+ },
+ {
+ "fields": [
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ }
+ ]
+ },
+ {
+ "fields": null
+ },
+ {
+ "fields": [
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ }
+ ]
+ },
+ {
+ "fields": [
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ }
+ ]
+ },
+ {
+ "fields": [
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ },
+ {
+ "appliedDirectives": []
+ }
+ ]
+ },
+ {
+ "fields": null
+ },
+ {
+ "fields": null
+ },
+ {
+ "fields": null
+ },
+ {
+ "fields": [
+ {
+ "appliedDirectives": [
+ {
+ "name": "foo",
+ "args": []
+ },
+ {
+ "name": "bar",
+ "args": [
+ {
+ "name": "baz",
+ "value": "\"ABC\""
+ }
+ ]
+ },
+ {
+ "name": "bar",
+ "args": []
+ },
+ {
+ "name": "bar",
+ "args": [
+ {
+ "name": "quox",
+ "value": "{ a: \"ABC\" }"
+ }
+ ]
+ },
+ {
+ "name": "bar",
+ "args": [
+ {
+ "name": "quox",
+ "value": "{ a: \"DEF\" }"
+ }
+ ]
+ },
+ {
+ "name": "bar",
+ "args": []
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "fields": null
+ },
+ {
+ "fields": null
+ },
+ {
+ "fields": null
+ }
+ ]
+ }
+ }
+}