diff --git a/common/schema-builder/pom.xml b/common/schema-builder/pom.xml
index 52422d7f0..48b0dfbcc 100644
--- a/common/schema-builder/pom.xml
+++ b/common/schema-builder/pom.xml
@@ -25,7 +25,14 @@
io.smallrye
jandex
-
+
+
+
+ org.jetbrains.kotlinx
+ kotlinx-metadata-jvm
+ ${version.kotlinx.metadata.jvm}
+
+
org.jboss.logging
@@ -61,6 +68,16 @@
jakarta.validation-api
test
+
+ io.smallrye.reactive
+ mutiny
+ test
+
+
+ jakarta.json
+ jakarta.json-api
+ test
+
@@ -73,6 +90,27 @@
false
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-plugin
+ ${version.kotlin.compiler}
+
+
+ test-compile
+
+ test-compile
+
+
+
+ ${project.basedir}/src/test/kotlin
+
+
+
+
+
+
+
diff --git a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/Annotations.java b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/Annotations.java
index 25f2636ff..a5821babf 100644
--- a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/Annotations.java
+++ b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/Annotations.java
@@ -594,6 +594,7 @@ private static Map getAnnotationsWithFilter(org.jbo
public static final DotName DIRECTIVE = DotName.createSimple("io.smallrye.graphql.api.Directive");
public static final DotName DEFAULT_NON_NULL = DotName.createSimple("io.smallrye.graphql.api.DefaultNonNull");
public static final DotName NULLABLE = DotName.createSimple("io.smallrye.graphql.api.Nullable");
+ public static final DotName KOTLIN_METADATA = DotName.createSimple("kotlin.Metadata");
// MicroProfile GraphQL Annotations
public static final DotName GRAPHQL_API = DotName.createSimple("org.eclipse.microprofile.graphql.GraphQLApi");
diff --git a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/OperationCreator.java b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/OperationCreator.java
index 77426702e..b946d6c63 100644
--- a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/OperationCreator.java
+++ b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/OperationCreator.java
@@ -5,6 +5,7 @@
import java.util.Objects;
import java.util.Optional;
+import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.DotName;
import org.jboss.jandex.MethodInfo;
import org.jboss.jandex.Type;
@@ -21,6 +22,14 @@
import io.smallrye.graphql.schema.model.Operation;
import io.smallrye.graphql.schema.model.OperationType;
import io.smallrye.graphql.schema.model.Reference;
+import kotlinx.metadata.Flag;
+import kotlinx.metadata.KmClassifier;
+import kotlinx.metadata.KmFunction;
+import kotlinx.metadata.KmType;
+import kotlinx.metadata.KmTypeProjection;
+import kotlinx.metadata.KmValueParameter;
+import kotlinx.metadata.jvm.KotlinClassHeader;
+import kotlinx.metadata.jvm.KotlinClassMetadata;
/**
* Creates a Operation object
@@ -94,9 +103,73 @@ public Operation createOperation(MethodInfo methodInfo, OperationType operationT
addDirectivesForRolesAllowed(annotationsForMethod, annotationsForClass, operation, reference);
populateField(Direction.OUT, operation, fieldType, annotationsForMethod);
+ if (operation.hasWrapper()) {
+ checkWrappedTypeKotlinNullability(methodInfo, annotationsForClass, operation);
+ }
return operation;
}
+ // If the operation return type is a wrapper and is written in Kotlin,
+ // this checks whether the wrapped type is nullable.
+ // Nullability metadata is stored in the kotlin.Metadata annotation
+ // on the class that contains the operation.
+ private void checkWrappedTypeKotlinNullability(MethodInfo methodInfo,
+ Annotations annotationsForClass,
+ Operation operation) {
+ Optional kotlinMetadataAnnotation = annotationsForClass
+ .getOneOfTheseAnnotations(Annotations.KOTLIN_METADATA);
+ if (kotlinMetadataAnnotation.isPresent()) {
+ KotlinClassMetadata.Class kotlinClass = toKotlinClassMetadata(kotlinMetadataAnnotation.get());
+ // We need to find the corresponding function inside
+ // the KotlinClassMetadata to check its IS_NULLABLE flag.
+ Optional function = kotlinClass.getKmClass().getFunctions()
+ .stream()
+ .filter(f -> f.getName().equals(methodInfo.name()))
+ .filter(f -> compareParameterLists(f.getValueParameters(), methodInfo.parameterTypes()))
+ .findAny();
+ if (function.isPresent()) {
+ KmType returnType = function.get().getReturnType();
+ KmTypeProjection arg = returnType.getArguments().get(0);
+ int flags = arg.getType().getFlags();
+ boolean nullable = Flag.Type.IS_NULLABLE.invoke(flags);
+ if (nullable) {
+ operation.setNotNull(false);
+ }
+ }
+ }
+ }
+
+ private boolean compareParameterLists(List kotlinParameters,
+ List jandexParameters) {
+ if (kotlinParameters.size() != jandexParameters.size()) {
+ return false;
+ }
+ for (int i = 0; i < kotlinParameters.size(); i++) {
+ // TODO: the matching of parameter types could use some improvements
+ // For example, it won't work for primitives.
+ // An Int parameter will be represented as kotlin.Int in the KotlinClassMetadata,
+ // but as "int" in the Jandex MethodInfo.
+ if (!((KmClassifier.Class) kotlinParameters.get(i).getType().classifier)
+ .getName().replace("/", ".")
+ .equals(jandexParameters.get(i).name().toString())) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private KotlinClassMetadata.Class toKotlinClassMetadata(AnnotationInstance metadata) {
+ KotlinClassHeader classHeader = new KotlinClassHeader(
+ metadata.value("k").asInt(),
+ metadata.value("mv").asIntArray(),
+ metadata.value("d1").asStringArray(),
+ metadata.value("d2").asStringArray(),
+ metadata.value("xs") != null ? metadata.value("xs").asString() : null,
+ metadata.value("pn") != null ? metadata.value("pn").asString() : null,
+ metadata.value("xi").asInt());
+ return (KotlinClassMetadata.Class) KotlinClassMetadata.read(classHeader);
+ }
+
private static void validateFieldType(MethodInfo methodInfo, OperationType operationType) {
Type returnType = methodInfo.returnType();
if (!operationType.equals(OperationType.MUTATION) && returnType.kind().equals(Type.Kind.VOID)) {
diff --git a/common/schema-builder/src/test/java/io/smallrye/graphql/index/SchemaBuilderTest.java b/common/schema-builder/src/test/java/io/smallrye/graphql/index/SchemaBuilderTest.java
index 185372afe..d538812e1 100644
--- a/common/schema-builder/src/test/java/io/smallrye/graphql/index/SchemaBuilderTest.java
+++ b/common/schema-builder/src/test/java/io/smallrye/graphql/index/SchemaBuilderTest.java
@@ -17,6 +17,7 @@
import java.nio.file.Paths;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.concurrent.ExecutorService;
@@ -269,6 +270,36 @@ public void testGenericSchemaBuilding() {
}
+ @Test
+ public void testKotlinTypeNullability() {
+ Indexer indexer = new Indexer();
+ indexDirectory(indexer, "io/smallrye/graphql/kotlin");
+ IndexView index = indexer.complete();
+ Schema schema = SchemaBuilder.build(index);
+
+ assertTrue(getQueryByName(schema, "notNullable").isNotNull());
+ assertFalse(getQueryByName(schema, "nullable").isNotNull());
+ assertTrue(getQueryByName(schema, "notNullableItemInUni").isNotNull());
+ assertFalse(getQueryByName(schema, "nullableItemInUni").isNotNull());
+
+ Map fooSubfields = schema.getTypes().get("Foo").getOperations();
+ assertTrue(fooSubfields.get("notNullableNestedItem").isNotNull());
+ assertTrue(fooSubfields.get("notNullableNestedItemInUni").isNotNull());
+ assertFalse(fooSubfields.get("nullableNestedItem").isNotNull());
+ assertFalse(fooSubfields.get("nullableNestedItemInUni").isNotNull());
+
+ assertFalse(getQueryByName(schema, "zzz1").isNotNull());
+ assertFalse(getQueryByName(schema, "zzz2").isNotNull());
+ assertTrue(getQueryByName(schema, "zzz3").isNotNull());
+ assertTrue(getQueryByName(schema, "zzz4").isNotNull());
+ }
+
+ private Operation getQueryByName(Schema schema, String name) {
+ return schema.getQueries()
+ .stream().filter(q -> q.getName().equals(name))
+ .findFirst().orElseThrow();
+ }
+
static IndexView getTCKIndex() {
Indexer indexer = new Indexer();
indexDirectory(indexer, "org/eclipse/microprofile/graphql/tck/apps/basic/api");
diff --git a/common/schema-builder/src/test/kotlin/io/smallrye/graphql/kotlin/Foo.kt b/common/schema-builder/src/test/kotlin/io/smallrye/graphql/kotlin/Foo.kt
new file mode 100644
index 000000000..40b6f766f
--- /dev/null
+++ b/common/schema-builder/src/test/kotlin/io/smallrye/graphql/kotlin/Foo.kt
@@ -0,0 +1,49 @@
+package io.smallrye.graphql.kotlin
+
+import io.smallrye.mutiny.Uni
+import org.eclipse.microprofile.graphql.GraphQLApi
+import org.eclipse.microprofile.graphql.Query
+import org.eclipse.microprofile.graphql.Source
+import jakarta.json.bind.annotation.JsonbCreator
+
+data class Foo @JsonbCreator constructor(val bar: String?)
+data class Foo2 @JsonbCreator constructor(val bar: String?)
+data class Foo3 @JsonbCreator constructor(val bar: String?)
+data class Foo4 @JsonbCreator constructor(val bar: String?)
+
+@GraphQLApi
+class Example {
+ @Query
+ fun nullable(): Foo? = null
+
+ @Query
+ fun notNullable(): Foo = Foo("blabla")
+
+ @Query
+ fun nullableItemInUni(): Uni = Uni.createFrom().nullItem()
+
+ @Query
+ fun notNullableItemInUni(): Uni = Uni.createFrom().item(Foo("blabla"))
+
+ fun nullableNestedItem(@Source foo: Foo): String? = foo.bar
+
+ fun notNullableNestedItem(@Source foo: Foo): String = "bar"
+
+ fun nullableNestedItemInUni(@Source foo: Foo): Uni = Uni.createFrom().nullItem()
+
+ fun notNullableNestedItemInUni(@Source foo: Foo): Uni = Uni.createFrom().item("bar")
+
+ // some overloaded methods to make sure we correctly find the function inside KotlinClassMetadata
+ @Query("zzz1")
+ fun zzz(x: Foo): Uni = Uni.createFrom().nullItem()
+
+ @Query("zzz2")
+ fun zzz(x: Foo2): Uni = Uni.createFrom().nullItem()
+
+ @Query("zzz3")
+ fun zzz(x: Foo3): Uni = Uni.createFrom().nullItem()
+
+ @Query("zzz4")
+ fun zzz(x: Foo4): Uni = Uni.createFrom().nullItem()
+
+}
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index e17f78669..f11c278b1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -65,6 +65,8 @@
11
11
1.8.0
+ 0.7.0
+ 1.9.0