Skip to content

Commit b06d2e1

Browse files
Allow deprecation of input values
As per the [September 2025 spec](https://github.com/graphql/graphql-spec/blob/main/changelogs/September2025.md). Also makes `includeDeprecated` arguments non-nullable booleans, as well as the `reason` argument on the `@deprecated` directive.
1 parent aee2694 commit b06d2e1

7 files changed

Lines changed: 290 additions & 45 deletions

File tree

‎modules/core/src/main/scala/compiler.scala‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1146,7 +1146,12 @@ object QueryCompiler {
11461146
Elab.transformChild(child => if (include) child else Filter(Eql(FieldTypeRef / "isDeprecated", Const(false)), child))
11471147
case (TypeTypeRef, "enumValues", List(Binding("includeDeprecated", BooleanValue(include)))) =>
11481148
Elab.transformChild(child => if (include) child else Filter(Eql(EnumValueTypeRef / "isDeprecated", Const(false)), child))
1149-
1149+
case (TypeTypeRef, "inputFields", List(Binding("includeDeprecated", BooleanValue(include)))) =>
1150+
Elab.transformChild(child => if (include) child else Filter(Eql(Introspection.__InputValueType / "isDeprecated", Const(false)), child))
1151+
case (FieldTypeRef, "args", List(Binding("includeDeprecated", BooleanValue(include)))) =>
1152+
Elab.transformChild(child => if (include) child else Filter(Eql(Introspection.__InputValueType / "isDeprecated", Const(false)), child))
1153+
case (Introspection.__DirectiveType, "args", List(Binding("includeDeprecated", BooleanValue(include)))) =>
1154+
Elab.transformChild(child => if (include) child else Filter(Eql(Introspection.__InputValueType / "isDeprecated", Const(false)), child))
11501155
case _ =>
11511156
Elab.unit
11521157
}

‎modules/core/src/main/scala/introspection.scala‎

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,29 +42,29 @@ object Introspection {
4242
name: String
4343
description: String
4444
45-
# OBJECT and INTERFACE only
46-
fields(includeDeprecated: Boolean = false): [__Field!]
45+
# must be non-null for OBJECT and INTERFACE, otherwise null.
46+
fields(includeDeprecated: Boolean! = false): [__Field!]
4747
48-
# OBJECT only
48+
# must be non-null for OBJECT and INTERFACE, otherwise null.
4949
interfaces: [__Type!]
5050
51-
# INTERFACE and UNION only
51+
# must be non-null for INTERFACE and UNION, otherwise null.
5252
possibleTypes: [__Type!]
5353
54-
# ENUM only
55-
enumValues(includeDeprecated: Boolean = false): [__EnumValue!]
54+
# must be non-null for ENUM, otherwise null.
55+
enumValues(includeDeprecated: Boolean! = false): [__EnumValue!]
5656
57-
# INPUT_OBJECT only
58-
inputFields: [__InputValue!]
57+
# must be non-null for INPUT_OBJECT, otherwise null.
58+
inputFields(includeDeprecated: Boolean! = false): [__InputValue!]
5959
60-
# NON_NULL and LIST only
60+
# must be non-null for NON_NULL and LIST, otherwise null.
6161
ofType: __Type
6262
}
6363
6464
type __Field {
6565
name: String!
6666
description: String
67-
args: [__InputValue!]!
67+
args(includeDeprecated: Boolean! = false): [__InputValue!]!
6868
type: __Type!
6969
isDeprecated: Boolean!
7070
deprecationReason: String
@@ -75,6 +75,8 @@ object Introspection {
7575
description: String
7676
type: __Type!
7777
defaultValue: String
78+
isDeprecated: Boolean!
79+
deprecationReason: String
7880
}
7981
8082
type __EnumValue {
@@ -99,7 +101,7 @@ object Introspection {
99101
name: String!
100102
description: String
101103
locations: [__DirectiveLocation!]!
102-
args: [__InputValue!]!
104+
args(includeDeprecated: Boolean! = false): [__InputValue!]!
103105
isRepeatable: Boolean!
104106
}
105107
@@ -111,6 +113,7 @@ object Introspection {
111113
FRAGMENT_DEFINITION
112114
FRAGMENT_SPREAD
113115
INLINE_FRAGMENT
116+
VARIABLE_DEFINITION
114117
SCHEMA
115118
SCALAR
116119
OBJECT
@@ -258,7 +261,9 @@ object Introspection {
258261
ValueField("name", _.name),
259262
ValueField("description", _.description),
260263
ValueField("type", _.tpe.dealias),
261-
ValueField("defaultValue", _.defaultValue.map(SchemaRenderer.renderValue))
264+
ValueField("defaultValue", _.defaultValue.map(SchemaRenderer.renderValue)),
265+
ValueField("isDeprecated", _.isDeprecated),
266+
ValueField("deprecationReason", _.deprecationReason)
262267
),
263268
ValueObjectMapping(__EnumValueType).on[EnumValueDefinition](
264269
ValueField("name", _.name),

‎modules/core/src/main/scala/schema.scala‎

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -764,6 +764,21 @@ sealed trait TypeWithFields extends NamedType {
764764
@deprecated("Use interfaces instead", "0.20.0")
765765
def allInterfaces: List[NamedType] = interfaces
766766
}
767+
/**
768+
* Mixin for types that can be deprecated.
769+
*/
770+
protected trait Deprecatable {
771+
def directives: List[Directive]
772+
773+
def deprecatedDirective: Option[Directive] =
774+
directives.find(_.name == "deprecated")
775+
def isDeprecated: Boolean = deprecatedDirective.isDefined
776+
def deprecationReason: Option[String] =
777+
for {
778+
dir <- deprecatedDirective
779+
reason <- dir.args.collectFirst { case Binding("reason", StringValue(reason)) => reason }
780+
} yield reason
781+
}
767782

768783
/**
769784
* Scalar extensions allow additional directives to be applied to a pre-existing Scalar type
@@ -897,16 +912,7 @@ case class EnumValueDefinition(
897912
name: String,
898913
description: Option[String],
899914
directives: List[Directive]
900-
) {
901-
def deprecatedDirective: Option[Directive] =
902-
directives.find(_.name == "deprecated")
903-
def isDeprecated: Boolean = deprecatedDirective.isDefined
904-
def deprecationReason: Option[String] =
905-
for {
906-
dir <- deprecatedDirective
907-
reason <- dir.args.collectFirst { case Binding("reason", StringValue(reason)) => reason }
908-
} yield reason
909-
}
915+
) extends Deprecatable
910916

911917
/**
912918
* Input objects are composite types used as inputs into queries defined as a list of named input
@@ -972,16 +978,7 @@ case class Field(
972978
args: List[InputValue],
973979
tpe: Type,
974980
directives: List[Directive]
975-
) {
976-
def deprecatedDirective: Option[Directive] =
977-
directives.find(_.name == "deprecated")
978-
def isDeprecated: Boolean = deprecatedDirective.isDefined
979-
def deprecationReason: Option[String] =
980-
for {
981-
dir <- deprecatedDirective
982-
reason <- dir.args.collectFirst { case Binding("reason", StringValue(reason)) => reason }
983-
} yield reason
984-
}
981+
) extends Deprecatable
985982

986983
/**
987984
* @param defaultValue a String encoding (using the GraphQL language) of the default value used by
@@ -993,7 +990,7 @@ case class InputValue(
993990
tpe: Type,
994991
defaultValue: Option[Value],
995992
directives: List[Directive]
996-
)
993+
) extends Deprecatable
997994

998995
sealed trait Value
999996

@@ -1249,9 +1246,9 @@ object DirectiveDef {
12491246
|fields on a type or deprecated enum values.
12501247
""".stripMargin.trim
12511248
),
1252-
List(InputValue("reason", Some("Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax, as specified by [CommonMark](https://commonmark.org/)."), NullableType(StringType), Some(StringValue("No longer supported")), Nil)),
1249+
List(InputValue("reason", Some("Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax, as specified by [CommonMark](https://commonmark.org/)."), StringType, Some(StringValue("No longer supported")), Nil)),
12531250
false,
1254-
List(DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.ENUM_VALUE)
1251+
List(DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.ARGUMENT_DEFINITION, DirectiveLocation.INPUT_FIELD_DEFINITION, DirectiveLocation.ENUM_VALUE)
12551252
)
12561253

12571254
val builtIns: List[DirectiveDef] =

‎modules/core/src/test/scala/compiler/DirectivesSuite.scala‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,40 @@ final class DirectivesSuite extends CatsEffectSuite {
149149
assertEquals(schema.map(testDirectiveDefs), List(expected).success)
150150
}
151151

152+
test("Directive definition with deprecated argument") {
153+
val expected =
154+
DirectiveDef(
155+
"foo",
156+
None,
157+
List(
158+
InputValue(
159+
"arg0",
160+
None,
161+
ScalarType.StringType,
162+
None,
163+
List(Directive("deprecated", List(Binding("reason", Value.StringValue("Use arg1 instead")))))
164+
),
165+
InputValue("arg1", None, ScalarType.StringType, None, Nil)
166+
),
167+
false,
168+
List(FIELD)
169+
)
170+
171+
val schema =
172+
Schema("""
173+
type Query {
174+
foo: Int
175+
}
176+
177+
directive @foo(
178+
arg0: String! @deprecated(reason: "Use arg1 instead"),
179+
arg1: String!
180+
) on FIELD
181+
""")
182+
183+
assertEquals(schema.map(testDirectiveDefs), List(expected).success)
184+
}
185+
152186
test("Schema with directives") {
153187
val schema =
154188
"""|schema @foo {

‎modules/core/src/test/scala/directives/DirectiveValidationSuite.scala‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ final class DirectiveValidationSuite extends CatsEffectSuite {
177177
f10: Int @deprecated
178178
f11: Int @deprecated(reason: 1)
179179
f12: Int @deprecated(x: "foo")
180+
f13: Int @deprecated(reason: null)
180181
}
181182
182183
directive @withArg(i: Int) on FIELD_DEFINITION
@@ -193,6 +194,7 @@ final class DirectiveValidationSuite extends CatsEffectSuite {
193194
Problem("""Unknown argument(s) 'x' in directive withRequiredArg"""),
194195
Problem("""Expected String found '1' for 'reason' in directive deprecated"""),
195196
Problem("""Unknown argument(s) 'x' in directive deprecated"""),
197+
Problem("""Expected String found 'null' for 'reason' in directive deprecated"""),
196198
)
197199

198200
assertEquals(schema.toProblems, problems)

0 commit comments

Comments
 (0)