From 1bc91baac6fd71e2595e990aeaf0d4aee8ef5480 Mon Sep 17 00:00:00 2001 From: sar9ho <122429141+sar9ho@users.noreply.github.com> Date: Sun, 21 Dec 2025 07:25:44 -0800 Subject: [PATCH 1/2] Generate required fields constructor when JSpecify is enabled * Add tests for JSpecify constructor generation based on field nullability * Generated required fields constructor when JSpecify enabled * Fix ktlint formatting in DataTypeGenerator * fixed ktlint formatting in CodeGenTest * gate required-fields constructor behind javaGenerateAllConstructor + avoid duplicates * added tests for required-fields constructor behavior --- .gitignore | 1 + .../generators/java/DataTypeGenerator.kt | 21 +++- .../graphql/dgs/codegen/CodeGenTest.kt | 110 ++++++++++++++++++ 3 files changed, 126 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 941bcfa24..4955aebc2 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ graphql-dgs-codegen-core/compiled-sources/ generated generated-examples scripts/__pycache__ +.kotlin/ diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/DataTypeGenerator.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/DataTypeGenerator.kt index b68e10b41..9a57e2689 100644 --- a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/DataTypeGenerator.kt +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/DataTypeGenerator.kt @@ -459,16 +459,25 @@ abstract class BaseDataTypeGenerator( addGetterAndSetter(it, javaType) } + val requiredFields = fields.filter { !it.nullable } + if (config.generateJSpecifyAnnotations) { - // Since JSpecify annotations are enabled, add a private no-arg constructor - // only for the Builder to access - addDefaultConstructor(javaType, false) + val allFieldsNullable = requiredFields.isEmpty() + addDefaultConstructor(javaType, allFieldsNullable) + + if (config.javaGenerateAllConstructor && fields.isNotEmpty() && fields.size < 256) { + addParameterizedConstructor(fields, javaType) + + if (requiredFields.isNotEmpty() && requiredFields.size < fields.size) { + addParameterizedConstructor(requiredFields, javaType) + } + } } else { addDefaultConstructor(javaType, true) - } - if (config.javaGenerateAllConstructor && fields.isNotEmpty() && fields.size < 256) { - addParameterizedConstructor(fields, javaType) + if (config.javaGenerateAllConstructor && fields.isNotEmpty() && fields.size < 256) { + addParameterizedConstructor(fields, javaType) + } } addToString(fields, javaType) diff --git a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/CodeGenTest.kt b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/CodeGenTest.kt index 135e98a6a..039637670 100644 --- a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/CodeGenTest.kt +++ b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/CodeGenTest.kt @@ -7061,4 +7061,114 @@ It takes a title and such. dataTypes[0].writeTo(System.out) assertCompilesJava(dataTypes) } + + @Test + fun `public no-arg constructor is generated when jspecify enabled and all fields are nullable`() { + val schema = + """ + type Foo { + a: String + b: Int + } + """.trimIndent() + + val result = + CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = BASE_PACKAGE_NAME, + language = Language.JAVA, + generateJSpecifyAnnotations = true, + javaGenerateAllConstructor = true, + ), + ).generate() + + val fooType = result.javaDataTypes.single { it.typeSpec().name() == "Foo" } + val constructors = fooType.typeSpec().methodSpecs().filter { it.isConstructor } + + assertThat(constructors).anySatisfy { ctor -> + assertThat(ctor.modifiers()).contains(javax.lang.model.element.Modifier.PUBLIC) + assertThat(ctor.parameters()).isEmpty() + } + + assertCompilesJava(result) + } + + @Test + fun `required-fields constructor is generated when jspecify enabled and non-null fields exist`() { + val schema = + """ + type Foo { + a: String! + b: Int + } + """.trimIndent() + + val result = + CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = BASE_PACKAGE_NAME, + language = Language.JAVA, + generateJSpecifyAnnotations = true, + javaGenerateAllConstructor = true, + ), + ).generate() + + val fooType = result.javaDataTypes.single { it.typeSpec().name() == "Foo" } + val constructors = fooType.typeSpec().methodSpecs().filter { it.isConstructor } + + assertThat(constructors).anySatisfy { ctor -> + assertThat(ctor.modifiers()).contains(javax.lang.model.element.Modifier.PUBLIC) + assertThat(ctor.parameters()).hasSize(1) + assertThat(ctor.parameters()[0].name()).isEqualTo("a") + } + + assertThat(constructors).anySatisfy { ctor -> + assertThat(ctor.modifiers()).contains(javax.lang.model.element.Modifier.PRIVATE) + assertThat(ctor.parameters()).isEmpty() + } + + assertCompilesJava(result) + } + + @Test + fun `required-fields constructor is not generated when all fields are required`() { + val schema = + """ + type Foo { + a: String! + b: Int! + } + """.trimIndent() + + val result = + CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = BASE_PACKAGE_NAME, + language = Language.JAVA, + generateJSpecifyAnnotations = true, + javaGenerateAllConstructor = true, + ), + ).generate() + + val fooType = result.javaDataTypes.single { it.typeSpec().name() == "Foo" } + val constructors = fooType.typeSpec().methodSpecs().filter { it.isConstructor } + + assertThat(constructors).anySatisfy { ctor -> + assertThat(ctor.modifiers()).contains(javax.lang.model.element.Modifier.PRIVATE) + assertThat(ctor.parameters()).isEmpty() + } + + val publicMultiArgCtors = + constructors.filter { ctor -> + ctor.modifiers().contains(javax.lang.model.element.Modifier.PUBLIC) && ctor.parameters().isNotEmpty() + } + + assertThat(publicMultiArgCtors).hasSize(1) + assertThat(publicMultiArgCtors.single().parameters()).hasSize(2) + + assertCompilesJava(result) + } } From 310dd4b6fcf7c26baa9690a07418a1b6b29a517f Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Tue, 3 Feb 2026 15:27:35 -0600 Subject: [PATCH 2/2] Polish gh-910 * expand conditional for generating required-args constructor to check requiredFields.size < 256 even if fields is not --- .../codegen/generators/java/DataTypeGenerator.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/DataTypeGenerator.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/DataTypeGenerator.kt index 9a57e2689..0d2581053 100644 --- a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/DataTypeGenerator.kt +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/DataTypeGenerator.kt @@ -459,16 +459,16 @@ abstract class BaseDataTypeGenerator( addGetterAndSetter(it, javaType) } - val requiredFields = fields.filter { !it.nullable } - if (config.generateJSpecifyAnnotations) { - val allFieldsNullable = requiredFields.isEmpty() - addDefaultConstructor(javaType, allFieldsNullable) + val requiredFields = fields.filter { !it.nullable } + addDefaultConstructor(javaType, requiredFields.isEmpty()) - if (config.javaGenerateAllConstructor && fields.isNotEmpty() && fields.size < 256) { - addParameterizedConstructor(fields, javaType) + if (config.javaGenerateAllConstructor) { + if (fields.isNotEmpty() && fields.size < 256) { + addParameterizedConstructor(fields, javaType) + } - if (requiredFields.isNotEmpty() && requiredFields.size < fields.size) { + if (requiredFields.isNotEmpty() && requiredFields.size < fields.size && requiredFields.size < 256) { addParameterizedConstructor(requiredFields, javaType) } }