diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 65dfbb199a..9b05f2a280 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,6 +15,7 @@ kotlinx-coroutines = "1.7.3" # TODO kotlin 1.9 upgrade -> kotlinx-serialization 1.6.0+ uses kotlin 1.9 kotlinx-serialization = "1.5.1" ktor = "2.3.10" +fastjson2 = "2.0.53" maven-plugin-annotation = "3.12.0" maven-plugin-api = "3.9.6" maven-project = "2.2.1" @@ -88,6 +89,8 @@ spring-boot-config = { group = "org.springframework.boot", name = "spring-boot-c spring-boot-netty = { group = "org.springframework.boot", name = "spring-boot-starter-reactor-netty", version.ref = "spring-boot" } spring-boot-webflux = { group = "org.springframework.boot", name = "spring-boot-starter-webflux", version.ref = "spring-boot" } spring-webflux = { group = "org.springframework", name = "spring-webflux", version.ref = "spring" } +fastjson2 = { group = "com.alibaba.fastjson2", name = "fastjson2-kotlin", version.ref = "fastjson2" } +fastjson2-spring = { group = "com.alibaba.fastjson2", name = "fastjson2-extension-spring6", version.ref = "fastjson2" } # test dependencies compile-testing = { group = "dev.zacsweers.kctfork", name = "core", version.ref = "compile-testing" } diff --git a/plugins/server/graphql-kotlin-graalvm-metadata-generator/src/main/resources/default-reflect-config.json b/plugins/server/graphql-kotlin-graalvm-metadata-generator/src/main/resources/default-reflect-config.json index 1de9aa68db..048aef1be3 100644 --- a/plugins/server/graphql-kotlin-graalvm-metadata-generator/src/main/resources/default-reflect-config.json +++ b/plugins/server/graphql-kotlin-graalvm-metadata-generator/src/main/resources/default-reflect-config.json @@ -120,22 +120,6 @@ "allDeclaredFields": true, "queryAllDeclaredMethods": true }, - { - "name":"com.expediagroup.graphql.server.types.GraphQLRequest", - "fields":[{"name":"Companion"}] - }, - { - "name":"com.expediagroup.graphql.server.types.GraphQLRequest$Companion", - "methods":[{"name":"serializer","parameterTypes":[] }] - }, - { - "name":"com.expediagroup.graphql.server.types.GraphQLBatchRequest", - "fields":[{"name":"Companion"}] - }, - { - "name":"com.expediagroup.graphql.server.types.GraphQLBatchRequest$Companion", - "methods":[{"name":"serializer","parameterTypes":[] }] - }, { "name": "com.expediagroup.graphql.server.types.GraphQLServerRequestDeserializer", "methods": [ diff --git a/servers/graphql-kotlin-server/build.gradle.kts b/servers/graphql-kotlin-server/build.gradle.kts index 7b5be62f5a..26e35ce6c6 100644 --- a/servers/graphql-kotlin-server/build.gradle.kts +++ b/servers/graphql-kotlin-server/build.gradle.kts @@ -5,7 +5,6 @@ description = "Common code for running a GraphQL server in any HTTP server frame plugins { id("com.expediagroup.graphql.conventions") alias(libs.plugins.benchmark) - alias(libs.plugins.kotlin.serialization) } dependencies { @@ -13,7 +12,7 @@ dependencies { api(projects.graphqlKotlinDataloaderInstrumentation) api(projects.graphqlKotlinAutomaticPersistedQueries) api(libs.jackson) - api(libs.kotlinx.serialization.json) + api(libs.fastjson2) testImplementation(libs.kotlinx.coroutines.test) testImplementation(libs.logback) } @@ -32,6 +31,14 @@ kotlin.sourceSets.getByName("benchmarks") { } benchmark { + configurations { + register("graphQLRequest") { + include("com.expediagroup.graphql.server.GraphQLServerRequest*") + } + register("graphQLResponse") { + include("com.expediagroup.graphql.server.GraphQLServerResponse*") + } + } targets { register("benchmarks") { this as JvmBenchmarkTarget diff --git a/servers/graphql-kotlin-server/src/benchmarks/kotlin/GraphQLServerRequestBatchDeserializationBenchmark.kt b/servers/graphql-kotlin-server/src/benchmarks/kotlin/GraphQLServerRequestBatchDeserializationBenchmark.kt new file mode 100644 index 0000000000..74797e9324 --- /dev/null +++ b/servers/graphql-kotlin-server/src/benchmarks/kotlin/GraphQLServerRequestBatchDeserializationBenchmark.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2024 Expedia, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expediagroup.graphql.server + +import com.alibaba.fastjson2.JSON +import com.alibaba.fastjson2.JSONWriter +import com.alibaba.fastjson2.to +import com.expediagroup.graphql.server.types.GraphQLServerRequest +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import org.openjdk.jmh.annotations.Benchmark +import org.openjdk.jmh.annotations.Fork +import org.openjdk.jmh.annotations.Measurement +import org.openjdk.jmh.annotations.Scope +import org.openjdk.jmh.annotations.Setup +import org.openjdk.jmh.annotations.State +import org.openjdk.jmh.annotations.Warmup +import java.util.concurrent.TimeUnit + +@State(Scope.Benchmark) +@Fork(value = 5, jvmArgsAppend = ["--add-modules=jdk.incubator.vector", "-Dfastjson2.readerVector=true"]) +@Warmup(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 4, time = 5, timeUnit = TimeUnit.SECONDS) +open class GraphQLServerRequestBatchDeserializationBenchmark { + private val mapper = jacksonObjectMapper() + private lateinit var request: String + private lateinit var batchRequest: String + + @Setup + fun setUp() { + JSON.config(JSONWriter.Feature.WriteNulls) + val loader = this::class.java.classLoader + val operation = loader.getResource("StarWarsDetails.graphql")!!.readText().replace("\n", "\\n") + val variables = loader.getResource("StarWarsDetailsVariables.json")!!.readText() + batchRequest = """ + [ + { "operationName": "StarWarsDetails", "query": "$operation", "variables": $variables }, + { "operationName": "StarWarsDetails", "query": "$operation", "variables": $variables }, + { "operationName": "StarWarsDetails", "query": "$operation", "variables": $variables }, + { "operationName": "StarWarsDetails", "query": "$operation", "variables": $variables } + ] + """.trimIndent() + } + + @Benchmark + fun JacksonDeserializeGraphQLBatchRequest(): GraphQLServerRequest = mapper.readValue(batchRequest) + + @Benchmark + fun FastJsonDeserializeGraphQLBatchRequest(): GraphQLServerRequest = batchRequest.to() +} diff --git a/servers/graphql-kotlin-server/src/benchmarks/kotlin/GraphQLServerRequestDeserializationBenchmark.kt b/servers/graphql-kotlin-server/src/benchmarks/kotlin/GraphQLServerRequestDeserializationBenchmark.kt index 29ef6cb568..fb852b4885 100644 --- a/servers/graphql-kotlin-server/src/benchmarks/kotlin/GraphQLServerRequestDeserializationBenchmark.kt +++ b/servers/graphql-kotlin-server/src/benchmarks/kotlin/GraphQLServerRequestDeserializationBenchmark.kt @@ -16,10 +16,12 @@ package com.expediagroup.graphql.server -import com.expediagroup.graphql.server.testtypes.GraphQLServerRequest +import com.alibaba.fastjson2.JSON +import com.alibaba.fastjson2.JSONWriter +import com.alibaba.fastjson2.to +import com.expediagroup.graphql.server.types.GraphQLServerRequest import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue -import kotlinx.serialization.json.Json import org.openjdk.jmh.annotations.Benchmark import org.openjdk.jmh.annotations.Fork import org.openjdk.jmh.annotations.Measurement @@ -30,16 +32,16 @@ import org.openjdk.jmh.annotations.Warmup import java.util.concurrent.TimeUnit @State(Scope.Benchmark) -@Fork(5) -@Warmup(iterations = 1, time = 5, timeUnit = TimeUnit.SECONDS) -@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) +@Fork(value = 5, jvmArgsAppend = ["--add-modules=jdk.incubator.vector", "-Dfastjson2.readerVector=true"]) +@Warmup(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 4, time = 5, timeUnit = TimeUnit.SECONDS) open class GraphQLServerRequestDeserializationBenchmark { private val mapper = jacksonObjectMapper() private lateinit var request: String - private lateinit var batchRequest: String @Setup fun setUp() { + JSON.config(JSONWriter.Feature.WriteNulls) val loader = this::class.java.classLoader val operation = loader.getResource("StarWarsDetails.graphql")!!.readText().replace("\n", "\\n") val variables = loader.getResource("StarWarsDetailsVariables.json")!!.readText() @@ -50,25 +52,11 @@ open class GraphQLServerRequestDeserializationBenchmark { "variables": $variables } """.trimIndent() - batchRequest = """ - [ - { "operationName": "StarWarsDetails", "query": "$operation", "variables": $variables }, - { "operationName": "StarWarsDetails", "query": "$operation", "variables": $variables }, - { "operationName": "StarWarsDetails", "query": "$operation", "variables": $variables }, - { "operationName": "StarWarsDetails", "query": "$operation", "variables": $variables } - ] - """.trimIndent() } @Benchmark fun JacksonDeserializeGraphQLRequest(): GraphQLServerRequest = mapper.readValue(request) @Benchmark - fun JacksonDeserializeGraphQLBatchRequest(): GraphQLServerRequest = mapper.readValue(batchRequest) - - @Benchmark - fun KSerializationDeserializeGraphQLRequest(): GraphQLServerRequest = Json.decodeFromString(request) - - @Benchmark - fun KSerializationDeserializeGraphQLBatchRequest(): GraphQLServerRequest = Json.decodeFromString(batchRequest) + fun FastJsonDeserializeGraphQLRequest(): GraphQLServerRequest = request.to() } diff --git a/servers/graphql-kotlin-server/src/benchmarks/kotlin/GraphQLServerRequestSerializationBenchmark.kt b/servers/graphql-kotlin-server/src/benchmarks/kotlin/GraphQLServerRequestSerializationBenchmark.kt deleted file mode 100644 index 02d2e80547..0000000000 --- a/servers/graphql-kotlin-server/src/benchmarks/kotlin/GraphQLServerRequestSerializationBenchmark.kt +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2024 Expedia, Inc - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.expediagroup.graphql.server - -import com.expediagroup.graphql.server.testtypes.GraphQLBatchRequest -import com.expediagroup.graphql.server.testtypes.GraphQLRequest -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.fasterxml.jackson.module.kotlin.readValue -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import org.openjdk.jmh.annotations.Benchmark -import org.openjdk.jmh.annotations.Fork -import org.openjdk.jmh.annotations.Measurement -import org.openjdk.jmh.annotations.Scope -import org.openjdk.jmh.annotations.Setup -import org.openjdk.jmh.annotations.State -import org.openjdk.jmh.annotations.Warmup -import java.util.concurrent.TimeUnit - -@State(Scope.Benchmark) -@Fork(5) -@Warmup(iterations = 1, time = 5, timeUnit = TimeUnit.SECONDS) -@Measurement(iterations = 5, time = 10, timeUnit = TimeUnit.SECONDS) -open class GraphQLServerRequestSerializationBenchmark { - private val mapper = jacksonObjectMapper() - private lateinit var request: GraphQLRequest - private lateinit var batchRequest: GraphQLBatchRequest - - @Setup - fun setUp() { - val loader = this::class.java.classLoader - val operation = loader.getResource("StarWarsDetails.graphql")!!.readText().replace("\n", "\\n") - val variables = mapper.readValue>( - loader.getResourceAsStream("StarWarsDetailsVariables.json")!! - ) - request = GraphQLRequest(operation, "StarWarsDetails", variables) - batchRequest = GraphQLBatchRequest( - GraphQLRequest(operation, "StarWarsDetails", variables), - GraphQLRequest(operation, "StarWarsDetails", variables), - GraphQLRequest(operation, "StarWarsDetails", variables), - GraphQLRequest(operation, "StarWarsDetails", variables) - ) - } - - @Benchmark - fun JacksonSerializeGraphQLRequest(): String = mapper.writeValueAsString(request) - - @Benchmark - fun JacksonSerializeGraphQLBatchRequest(): String = mapper.writeValueAsString(batchRequest) - - @Benchmark - fun KSerializationSerializeGraphQLRequest(): String = Json.encodeToString(request) - - @Benchmark - fun KSerializationSerializeGraphQLBatchRequest(): String = Json.encodeToString(batchRequest) -} diff --git a/servers/graphql-kotlin-server/src/benchmarks/kotlin/GraphQLServerResponseDeserializationBenchmark.kt b/servers/graphql-kotlin-server/src/benchmarks/kotlin/GraphQLServerResponseBatchSerializationBenchmark.kt similarity index 50% rename from servers/graphql-kotlin-server/src/benchmarks/kotlin/GraphQLServerResponseDeserializationBenchmark.kt rename to servers/graphql-kotlin-server/src/benchmarks/kotlin/GraphQLServerResponseBatchSerializationBenchmark.kt index 1121136c1d..23b63a4aef 100644 --- a/servers/graphql-kotlin-server/src/benchmarks/kotlin/GraphQLServerResponseDeserializationBenchmark.kt +++ b/servers/graphql-kotlin-server/src/benchmarks/kotlin/GraphQLServerResponseBatchSerializationBenchmark.kt @@ -16,10 +16,12 @@ package com.expediagroup.graphql.server -import com.expediagroup.graphql.server.testtypes.GraphQLServerResponse +import com.alibaba.fastjson2.JSON +import com.alibaba.fastjson2.JSONWriter +import com.expediagroup.graphql.server.types.GraphQLBatchResponse +import com.expediagroup.graphql.server.types.GraphQLResponse import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue -import kotlinx.serialization.json.Json import org.openjdk.jmh.annotations.Benchmark import org.openjdk.jmh.annotations.Fork import org.openjdk.jmh.annotations.Measurement @@ -30,36 +32,32 @@ import org.openjdk.jmh.annotations.Warmup import java.util.concurrent.TimeUnit @State(Scope.Benchmark) -@Fork(5) -@Warmup(iterations = 1, time = 5, timeUnit = TimeUnit.SECONDS) -@Measurement(iterations = 5, time = 10, timeUnit = TimeUnit.SECONDS) -open class GraphQLServerResponseDeserializationBenchmark { +@Fork(value = 5, jvmArgsAppend = ["--add-modules=jdk.incubator.vector", "-Dfastjson2.readerVector=true"]) +@Warmup(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 4, time = 5, timeUnit = TimeUnit.SECONDS) +open class GraphQLServerResponseBatchSerializationBenchmark { private val mapper = jacksonObjectMapper() - private lateinit var response: String - private lateinit var batchResponse: String + private lateinit var batchResponse: GraphQLBatchResponse @Setup fun setUp() { - response = this::class.java.classLoader.getResource("StarWarsDetailsResponse.json")!!.readText() - batchResponse = """ - [ - $response, - $response, - $response, - $response - ] - """.trimIndent() + JSON.config(JSONWriter.Feature.WriteNulls) + val data = mapper.readValue>( + this::class.java.classLoader.getResourceAsStream("StarWarsDetailsResponse.json")!! + ) + batchResponse = GraphQLBatchResponse( + listOf( + GraphQLResponse(data), + GraphQLResponse(data), + GraphQLResponse(data), + GraphQLResponse(data) + ) + ) } @Benchmark - fun JacksonDeserializeGraphQLResponse(): GraphQLServerResponse = mapper.readValue(response) + fun JacksonSerializeGraphQLBatchResponse(): String = mapper.writeValueAsString(batchResponse) @Benchmark - fun JacksonDeserializeGraphQLBatchResponse(): GraphQLServerResponse = mapper.readValue(batchResponse) - - @Benchmark - fun KSerializationDeserializeGraphQLResponse(): GraphQLServerResponse = Json.decodeFromString(response) - - @Benchmark - fun KSerializationDeserializeGraphQLBatchResponse(): GraphQLServerResponse = Json.decodeFromString(batchResponse) + fun FastJsonSerializeGraphQLBatchResponse(): String = JSON.toJSONString(batchResponse) } diff --git a/servers/graphql-kotlin-server/src/benchmarks/kotlin/GraphQLServerResponseSerializationBenchmark.kt b/servers/graphql-kotlin-server/src/benchmarks/kotlin/GraphQLServerResponseSerializationBenchmark.kt index c3b78b0129..e522eddd67 100644 --- a/servers/graphql-kotlin-server/src/benchmarks/kotlin/GraphQLServerResponseSerializationBenchmark.kt +++ b/servers/graphql-kotlin-server/src/benchmarks/kotlin/GraphQLServerResponseSerializationBenchmark.kt @@ -16,12 +16,11 @@ package com.expediagroup.graphql.server -import com.expediagroup.graphql.server.testtypes.GraphQLBatchResponse -import com.expediagroup.graphql.server.testtypes.GraphQLResponse +import com.alibaba.fastjson2.JSON +import com.alibaba.fastjson2.JSONWriter +import com.expediagroup.graphql.server.types.GraphQLResponse import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json import org.openjdk.jmh.annotations.Benchmark import org.openjdk.jmh.annotations.Fork import org.openjdk.jmh.annotations.Measurement @@ -32,16 +31,16 @@ import org.openjdk.jmh.annotations.Warmup import java.util.concurrent.TimeUnit @State(Scope.Benchmark) -@Fork(5) -@Warmup(iterations = 1, time = 5, timeUnit = TimeUnit.SECONDS) -@Measurement(iterations = 5, time = 10, timeUnit = TimeUnit.SECONDS) +@Fork(value = 5, jvmArgsAppend = ["--add-modules=jdk.incubator.vector", "-Dfastjson2.readerVector=true"]) +@Warmup(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 4, time = 5, timeUnit = TimeUnit.SECONDS) open class GraphQLServerResponseSerializationBenchmark { private val mapper = jacksonObjectMapper() - private lateinit var response: GraphQLResponse - private lateinit var batchResponse: GraphQLBatchResponse + private lateinit var response: GraphQLResponse> @Setup fun setUp() { + JSON.config(JSONWriter.Feature.WriteNulls) val data = mapper.readValue>( this::class.java.classLoader.getResourceAsStream("StarWarsDetailsResponse.json")!! ) @@ -50,23 +49,11 @@ open class GraphQLServerResponseSerializationBenchmark { this::class.java.classLoader.getResourceAsStream("StarWarsDetailsResponse.json")!! ) ) - batchResponse = GraphQLBatchResponse( - GraphQLResponse(data), - GraphQLResponse(data), - GraphQLResponse(data), - GraphQLResponse(data) - ) } @Benchmark fun JacksonSerializeGraphQLResponse(): String = mapper.writeValueAsString(response) @Benchmark - fun JacksonSerializeGraphQLBatchResponse(): String = mapper.writeValueAsString(batchResponse) - - @Benchmark - fun KSerializationSerializeGraphQLResponse(): String = Json.encodeToString(response) - - @Benchmark - fun KSerializationSerializeGraphQLBatchResponse(): String = Json.encodeToString(batchResponse) + fun FastJsonSerializeGraphQLResponse(): String = JSON.toJSONString(response) } diff --git a/servers/graphql-kotlin-server/src/benchmarks/kotlin/testtypes/GraphQLServerRequest.kt b/servers/graphql-kotlin-server/src/benchmarks/kotlin/testtypes/GraphQLServerRequest.kt deleted file mode 100644 index 2d3e3cc6e2..0000000000 --- a/servers/graphql-kotlin-server/src/benchmarks/kotlin/testtypes/GraphQLServerRequest.kt +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2024 Expedia, Inc - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.expediagroup.graphql.server.testtypes - -import com.expediagroup.graphql.server.types.serializers.AnyNullableKSerializer -import com.fasterxml.jackson.annotation.JsonCreator -import com.fasterxml.jackson.annotation.JsonIgnoreProperties -import com.fasterxml.jackson.annotation.JsonInclude -import com.fasterxml.jackson.annotation.JsonValue -import com.fasterxml.jackson.core.JsonParser -import com.fasterxml.jackson.databind.DeserializationContext -import com.fasterxml.jackson.databind.JsonDeserializer -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.annotation.JsonDeserialize -import kotlinx.serialization.KSerializer -import kotlinx.serialization.Serializable -import kotlinx.serialization.SerializationException -import kotlinx.serialization.builtins.ListSerializer -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.buildClassSerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonDecoder -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.decodeFromJsonElement - -@JsonDeserialize(using = GraphQLServerRequestDeserializer::class) -@Serializable(with = GraphQLServerRequestKSerializer::class) -sealed class GraphQLServerRequest - -@JsonIgnoreProperties(ignoreUnknown = true) -@JsonInclude(JsonInclude.Include.NON_NULL) -@JsonDeserialize(using = JsonDeserializer.None::class) -@Serializable -data class GraphQLRequest( - val query: String = "", - val operationName: String? = null, - val variables: Map? = null, - val extensions: Map? = null -) : GraphQLServerRequest() - -@JsonIgnoreProperties(ignoreUnknown = true) -@JsonInclude(JsonInclude.Include.NON_NULL) -@JsonDeserialize(using = JsonDeserializer.None::class) -@Serializable(with = GraphQLBatchRequestKSerializer::class) -data class GraphQLBatchRequest @JsonCreator constructor(@get:JsonValue val requests: List) : GraphQLServerRequest() { - constructor(vararg requests: GraphQLRequest) : this(requests.toList()) -} - -class GraphQLServerRequestDeserializer : JsonDeserializer() { - override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): GraphQLServerRequest { - val codec = parser.codec - val jsonNode = codec.readTree(parser) - return if (jsonNode.isArray) { - codec.treeToValue(jsonNode, GraphQLBatchRequest::class.java) - } else { - codec.treeToValue(jsonNode, GraphQLRequest::class.java) - } - } -} - -object GraphQLServerRequestKSerializer : KSerializer { - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("GraphQLServerRequest") - - override fun deserialize(decoder: Decoder): GraphQLServerRequest { - val jsonDecoder = decoder as JsonDecoder - return when (val jsonElement = jsonDecoder.decodeJsonElement()) { - is JsonObject -> { - Json.decodeFromJsonElement(jsonElement) - } - is JsonArray -> { - GraphQLBatchRequest(Json.decodeFromJsonElement>(jsonElement)) - } - else -> throw SerializationException("Unknown JSON element found") - } - } - - override fun serialize( - encoder: Encoder, - value: GraphQLServerRequest, - ) { - when (value) { - is GraphQLRequest -> { - encoder.encodeSerializableValue(GraphQLRequest.serializer(), value) - } - is GraphQLBatchRequest -> { - encoder.encodeSerializableValue(ListSerializer(GraphQLRequest.serializer()), value.requests) - } - } - } -} - -object GraphQLBatchRequestKSerializer : KSerializer { - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("GraphQLBatchRequest") - - override fun deserialize(decoder: Decoder): GraphQLBatchRequest = - GraphQLBatchRequest(decoder.decodeSerializableValue(ListSerializer(GraphQLRequest.serializer()))) - - override fun serialize(encoder: Encoder, value: GraphQLBatchRequest) { - encoder.encodeSerializableValue(ListSerializer(GraphQLRequest.serializer()), value.requests) - } -} diff --git a/servers/graphql-kotlin-server/src/benchmarks/kotlin/testtypes/GraphQLServerResponse.kt b/servers/graphql-kotlin-server/src/benchmarks/kotlin/testtypes/GraphQLServerResponse.kt deleted file mode 100644 index e3a54975e9..0000000000 --- a/servers/graphql-kotlin-server/src/benchmarks/kotlin/testtypes/GraphQLServerResponse.kt +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Copyright 2024 Expedia, Inc - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.expediagroup.graphql.server.testtypes - -import com.expediagroup.graphql.server.types.serializers.AnyNullableKSerializer -import com.fasterxml.jackson.annotation.JsonCreator -import com.fasterxml.jackson.annotation.JsonIgnoreProperties -import com.fasterxml.jackson.annotation.JsonInclude -import com.fasterxml.jackson.annotation.JsonValue -import com.fasterxml.jackson.core.JsonParser -import com.fasterxml.jackson.databind.DeserializationContext -import com.fasterxml.jackson.databind.JsonDeserializer -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.annotation.JsonDeserialize -import kotlinx.serialization.KSerializer -import kotlinx.serialization.Serializable -import kotlinx.serialization.SerializationException -import kotlinx.serialization.builtins.ListSerializer -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.buildClassSerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonDecoder -import kotlinx.serialization.json.JsonEncoder -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.decodeFromJsonElement - -@JsonDeserialize(using = GraphQLServerResponseDeserializer::class) -@Serializable(with = GraphQLServerResponseKSerializer::class) -sealed class GraphQLServerResponse - -@JsonIgnoreProperties(ignoreUnknown = true) -@JsonInclude(JsonInclude.Include.NON_NULL) -@JsonDeserialize(using = JsonDeserializer.None::class) -@Serializable -data class GraphQLResponse( - val data: Map? = null, - val errors: List? = null, - val extensions: Map? = null -) : GraphQLServerResponse() - -@JsonIgnoreProperties(ignoreUnknown = true) -@JsonInclude(JsonInclude.Include.NON_NULL) -@JsonDeserialize(using = JsonDeserializer.None::class) -@Serializable(with = GraphQLBatchResponseKSerializer::class) -data class GraphQLBatchResponse @JsonCreator constructor(@get:JsonValue val responses: List) : GraphQLServerResponse() { - constructor(vararg responses: GraphQLResponse) : this(responses.toList()) -} - -@JsonIgnoreProperties(ignoreUnknown = true) -@JsonInclude(JsonInclude.Include.NON_NULL) -@Serializable -data class GraphQLServerError( - val message: String, - val locations: List? = null, - val path: List<@Serializable(with = GraphQLErrorPathKSerializer::class) Any>? = null, - val extensions: Map? = null -) - -@JsonIgnoreProperties(ignoreUnknown = true) -@Serializable -data class GraphQLSourceLocation( - val line: Int, - val column: Int -) - -class GraphQLServerResponseDeserializer : JsonDeserializer() { - override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): GraphQLServerResponse { - val codec = parser.codec - val jsonNode = codec.readTree(parser) - return if (jsonNode.isArray) { - codec.treeToValue(jsonNode, GraphQLBatchResponse::class.java) - } else { - codec.treeToValue(jsonNode, GraphQLResponse::class.java) - } - } -} - -object GraphQLServerResponseKSerializer : KSerializer { - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("GraphQLServerResponse") - - override fun deserialize(decoder: Decoder): GraphQLServerResponse { - val jsonDecoder = decoder as JsonDecoder - return when (val jsonElement = jsonDecoder.decodeJsonElement()) { - is JsonObject -> Json.decodeFromJsonElement(jsonElement) - is JsonArray -> GraphQLBatchResponse(Json.decodeFromJsonElement>(jsonElement)) - else -> throw SerializationException("Unknown JSON element found") - } - } - - override fun serialize( - encoder: Encoder, - value: GraphQLServerResponse, - ) { - when (value) { - is GraphQLResponse -> encoder.encodeSerializableValue(GraphQLResponse.serializer(), value) - is GraphQLBatchResponse -> encoder.encodeSerializableValue(ListSerializer(GraphQLResponse.serializer()), value.responses) - } - } -} - -object GraphQLBatchResponseKSerializer : KSerializer { - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("GraphQLBatchResponse") - - override fun deserialize(decoder: Decoder): GraphQLBatchResponse = - GraphQLBatchResponse(decoder.decodeSerializableValue(ListSerializer(GraphQLResponse.serializer()))) - - override fun serialize(encoder: Encoder, value: GraphQLBatchResponse) { - encoder.encodeSerializableValue(ListSerializer(GraphQLResponse.serializer()), value.responses) - } -} - -class GraphQLErrorPathKSerializer : KSerializer { - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("GraphQLErrorPath") - - override fun serialize(encoder: Encoder, value: Any) { - val jsonEncoder = encoder as JsonEncoder - val jsonElement = when (value) { - is Int -> JsonPrimitive(value) - is String -> JsonPrimitive(value) - else -> { - // should never be the case - JsonPrimitive(value.toString()) - } - } - jsonEncoder.encodeJsonElement(jsonElement) - } - - override fun deserialize(decoder: Decoder): Any { - val jsonDecoder = decoder as JsonDecoder - val element = jsonDecoder.decodeJsonElement() as JsonPrimitive - return if (!element.isString) { - element.content.toIntOrNull() ?: element.content - } else { - element.content - } - } -} diff --git a/servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/extensions/jsonReaderExtensions.kt b/servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/extensions/jsonReaderExtensions.kt new file mode 100644 index 0000000000..16627abfd3 --- /dev/null +++ b/servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/extensions/jsonReaderExtensions.kt @@ -0,0 +1,11 @@ +package com.expediagroup.graphql.server.extensions + +import com.alibaba.fastjson2.JSONReader + +inline fun JSONReader.readAsArray(): List { + val collector = mutableListOf() + readArray(collector, T::class.java) + return collector +} + +inline fun JSONReader.readAs(): T = read(T::class.java) diff --git a/servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/types/GraphQLServerError.kt b/servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/types/GraphQLServerError.kt index 518a98b00b..47abd4a3df 100644 --- a/servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/types/GraphQLServerError.kt +++ b/servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/types/GraphQLServerError.kt @@ -16,6 +16,8 @@ package com.expediagroup.graphql.server.types +import com.alibaba.fastjson2.annotation.JSONType +import com.expediagroup.graphql.server.types.serializers.FastJsonIncludeNonNullProperty import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonInclude @@ -26,6 +28,7 @@ import com.fasterxml.jackson.annotation.JsonInclude */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) +@JSONType(serializeFilters = [FastJsonIncludeNonNullProperty::class]) data class GraphQLServerError( val message: String, val locations: List? = null, diff --git a/servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/types/GraphQLServerRequest.kt b/servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/types/GraphQLServerRequest.kt index d8adb1e4a3..3c6cfd08c6 100644 --- a/servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/types/GraphQLServerRequest.kt +++ b/servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/types/GraphQLServerRequest.kt @@ -16,7 +16,12 @@ package com.expediagroup.graphql.server.types -import com.expediagroup.graphql.server.types.serializers.AnyNullableKSerializer +import com.alibaba.fastjson2.JSONReader +import com.alibaba.fastjson2.annotation.JSONType +import com.alibaba.fastjson2.reader.ObjectReader +import com.expediagroup.graphql.server.extensions.readAs +import com.expediagroup.graphql.server.extensions.readAsArray +import com.expediagroup.graphql.server.types.serializers.FastJsonIncludeNonNullProperty import com.fasterxml.jackson.annotation.JsonCreator import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonInclude @@ -26,25 +31,13 @@ import com.fasterxml.jackson.databind.DeserializationContext import com.fasterxml.jackson.databind.JsonDeserializer import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.annotation.JsonDeserialize -import kotlinx.serialization.KSerializer -import kotlinx.serialization.Serializable -import kotlinx.serialization.SerializationException -import kotlinx.serialization.builtins.ListSerializer -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.buildClassSerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonDecoder -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.decodeFromJsonElement +import java.lang.reflect.Type /** * GraphQL server request abstraction that provides a convenient way to handle both single and batch requests. */ @JsonDeserialize(using = GraphQLServerRequestDeserializer::class) -@Serializable(with = GraphQLServerRequestKSerializer::class) +@JSONType(deserializer = FastJsonGraphQLServerRequestDeserializer::class) sealed class GraphQLServerRequest /** @@ -53,12 +46,12 @@ sealed class GraphQLServerRequest @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) @JsonDeserialize(using = JsonDeserializer.None::class) -@Serializable +@JSONType(serializeFilters = [FastJsonIncludeNonNullProperty::class]) data class GraphQLRequest( val query: String = "", val operationName: String? = null, - val variables: Map? = null, - val extensions: Map? = null + val variables: Map? = null, + val extensions: Map? = null ) : GraphQLServerRequest() /** @@ -67,7 +60,6 @@ data class GraphQLRequest( @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) @JsonDeserialize(using = JsonDeserializer.None::class) -@Serializable(with = GraphQLBatchRequestKSerializer::class) data class GraphQLBatchRequest @JsonCreator constructor(@get:JsonValue val requests: List) : GraphQLServerRequest() { constructor(vararg requests: GraphQLRequest) : this(requests.toList()) } @@ -84,33 +76,18 @@ class GraphQLServerRequestDeserializer : JsonDeserializer( } } -object GraphQLServerRequestKSerializer : KSerializer { - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("GraphQLServerRequest") - - override fun deserialize(decoder: Decoder): GraphQLServerRequest { - val jsonDecoder = decoder as JsonDecoder - return when (val jsonElement = jsonDecoder.decodeJsonElement()) { - is JsonObject -> Json.decodeFromJsonElement(jsonElement) - is JsonArray -> GraphQLBatchRequest(Json.decodeFromJsonElement>(jsonElement)) - else -> throw SerializationException("Unknown JSON element found") - } - } - - override fun serialize(encoder: Encoder, value: GraphQLServerRequest) { - when (value) { - is GraphQLRequest -> encoder.encodeSerializableValue(GraphQLRequest.serializer(), value) - is GraphQLBatchRequest -> encoder.encodeSerializableValue(ListSerializer(GraphQLRequest.serializer()), value.requests) +object FastJsonGraphQLServerRequestDeserializer : ObjectReader { + override fun readObject( + jsonReader: JSONReader?, + fieldType: Type?, + fieldName: Any?, + features: Long + ): GraphQLServerRequest? { + if (jsonReader == null || jsonReader.nextIfNull()) return null + return if (jsonReader.isArray) { + GraphQLBatchRequest(jsonReader.readAsArray()) + } else { + jsonReader.readAs() } } } - -object GraphQLBatchRequestKSerializer : KSerializer { - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("GraphQLBatchRequest") - - override fun deserialize(decoder: Decoder): GraphQLBatchRequest = - GraphQLBatchRequest(decoder.decodeSerializableValue(ListSerializer(GraphQLRequest.serializer()))) - - override fun serialize(encoder: Encoder, value: GraphQLBatchRequest) { - encoder.encodeSerializableValue(ListSerializer(GraphQLRequest.serializer()), value.requests) - } -} diff --git a/servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/types/GraphQLServerResponse.kt b/servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/types/GraphQLServerResponse.kt index 742d549cb0..4a67c5dca0 100644 --- a/servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/types/GraphQLServerResponse.kt +++ b/servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/types/GraphQLServerResponse.kt @@ -16,6 +16,8 @@ package com.expediagroup.graphql.server.types +import com.alibaba.fastjson2.annotation.JSONType +import com.expediagroup.graphql.server.types.serializers.FastJsonIncludeNonNullProperty import com.fasterxml.jackson.annotation.JsonCreator import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonInclude @@ -38,6 +40,7 @@ sealed class GraphQLServerResponse @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) @JsonDeserialize(using = JsonDeserializer.None::class) +@JSONType(serializeFilters = [FastJsonIncludeNonNullProperty::class]) data class GraphQLResponse( val data: T? = null, val errors: List? = null, diff --git a/servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/types/serializers/AnyNullableKSerializer.kt b/servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/types/serializers/AnyNullableKSerializer.kt deleted file mode 100644 index db9c493228..0000000000 --- a/servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/types/serializers/AnyNullableKSerializer.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.expediagroup.graphql.server.types.serializers - -import com.expediagroup.graphql.generator.scalars.ID -import kotlinx.serialization.KSerializer -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.buildClassSerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonDecoder -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonEncoder -import kotlinx.serialization.json.JsonNull -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive - -object AnyNullableKSerializer : KSerializer { - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("AnyNullable") - - override fun serialize( - encoder: Encoder, - value: Any?, - ) { - val jsonEncoder = encoder as JsonEncoder - jsonEncoder.encodeJsonElement(serializeAny(value)) - } - - private fun serializeAny(value: Any?): JsonElement = - when (value) { - null -> JsonNull - is Map<*, *> -> { - val mapContents = - value.mapNotNull { (key, value) -> - key.toString() to serializeAny(value) - }.toMap() - JsonObject(mapContents) - } - is List<*> -> { - val arrayContents = value.mapNotNull { listEntry -> serializeAny(listEntry) } - JsonArray(arrayContents) - } - is Number -> JsonPrimitive(value) - is Boolean -> JsonPrimitive(value) - is String -> JsonPrimitive(value) - is ID -> JsonPrimitive(value.value) - else -> JsonNull - } - - override fun deserialize(decoder: Decoder): Any? { - val jsonDecoder = decoder as JsonDecoder - val element = jsonDecoder.decodeJsonElement() - return deserializeJsonElement(element) - } - - private fun deserializeJsonElement(element: JsonElement): Any? = - when (element) { - is JsonNull -> null - is JsonObject -> { - element.mapValues { deserializeJsonElement(it.value) } - } - is JsonArray -> { - element.map { deserializeJsonElement(it) } - } - is JsonPrimitive -> - when { - element.isString -> element.content - element.content == "true" -> true - element.content == "false" -> false - else -> { - element.content.toIntOrNull() ?: element.content.toLongOrNull() ?: element.content.toDoubleOrNull() ?: element.content - } - } - } -} diff --git a/servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/types/serializers/FastJsonIncludeNonNullProperty.kt b/servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/types/serializers/FastJsonIncludeNonNullProperty.kt new file mode 100644 index 0000000000..b85b5a5915 --- /dev/null +++ b/servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/types/serializers/FastJsonIncludeNonNullProperty.kt @@ -0,0 +1,11 @@ +package com.expediagroup.graphql.server.types.serializers + +import com.alibaba.fastjson2.filter.PropertyFilter + +class FastJsonIncludeNonNullProperty : PropertyFilter { + override fun apply( + `object`: Any?, + name: String?, + value: Any?, + ): Boolean = value != null +} diff --git a/servers/graphql-kotlin-server/src/test/kotlin/com/expediagroup/graphql/server/types/GraphQLServerRequestTest.kt b/servers/graphql-kotlin-server/src/test/kotlin/com/expediagroup/graphql/server/types/GraphQLServerRequestTest.kt index 7ef686fcdc..f06b91518a 100644 --- a/servers/graphql-kotlin-server/src/test/kotlin/com/expediagroup/graphql/server/types/GraphQLServerRequestTest.kt +++ b/servers/graphql-kotlin-server/src/test/kotlin/com/expediagroup/graphql/server/types/GraphQLServerRequestTest.kt @@ -16,9 +16,12 @@ package com.expediagroup.graphql.server.types +import com.alibaba.fastjson2.JSON +import com.alibaba.fastjson2.JSONWriter +import com.alibaba.fastjson2.to import com.expediagroup.graphql.generator.scalars.ID -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue import org.junit.jupiter.api.Test import kotlin.test.assertEquals import kotlin.test.assertNull @@ -26,6 +29,12 @@ import kotlin.test.assertTrue class GraphQLServerRequestTest { + init { + JSON.config(JSONWriter.Feature.WriteNulls) + } + + private val mapper = jacksonObjectMapper() + @Test fun `verify simple serialization`() { val request = GraphQLRequest( @@ -35,7 +44,7 @@ class GraphQLServerRequestTest { val expectedJson = """{"query":"{ foo }"}""" - assertEquals(expectedJson, Json.encodeToString(request)) + assertEquals(expectedJson, mapper.writeValueAsString(request)) } @Test @@ -48,8 +57,7 @@ class GraphQLServerRequestTest { val expectedJson = """{"query":"query FooQuery(${'$'}input: Int) { foo(${'$'}input) }","operationName":"FooQuery","variables":{"input":1}}""" - - assertEquals(expectedJson, Json.encodeToString(request)) + assertEquals(expectedJson, mapper.writeValueAsString(request)) } @Test @@ -63,7 +71,7 @@ class GraphQLServerRequestTest { val expectedJson = """{"query":"query FooQuery(${'$'}input: ID) { foo(${'$'}input) }","operationName":"FooQuery","variables":{"input":"1"}}""" - assertEquals(expectedJson, Json.encodeToString(request)) + assertEquals(expectedJson, mapper.writeValueAsString(request)) } @Test @@ -82,7 +90,7 @@ class GraphQLServerRequestTest { ) val expectedJson = """[{"query":"query FooQuery(${'$'}input: Int) { foo(${'$'}input) }","operationName":"FooQuery","variables":{"input":1}},{"query":"query BarQuery { bar }"}]""" - assertEquals(expectedJson, Json.encodeToString(request)) + assertEquals(expectedJson, mapper.writeValueAsString(request)) } @Test @@ -90,12 +98,17 @@ class GraphQLServerRequestTest { val input = """{"query":"{ foo }"}""" - val request = Json.decodeFromString(input) - + val request = mapper.readValue(input) assertTrue(request is GraphQLRequest) assertEquals("{ foo }", request.query) assertNull(request.operationName) assertNull(request.variables) + + val requestFastJson = input.to() + assertTrue(requestFastJson is GraphQLRequest) + assertEquals("{ foo }", requestFastJson.query) + assertNull(requestFastJson.operationName) + assertNull(requestFastJson.variables) } @Test @@ -103,12 +116,17 @@ class GraphQLServerRequestTest { val input = """{"query":"query FooQuery(${'$'}input: Int) { foo(${'$'}input) }","operationName":"FooQuery","variables":{"input":1}}""" - val request = Json.decodeFromString(input) - + val request = mapper.readValue(input) assertTrue(request is GraphQLRequest) assertEquals("query FooQuery(\$input: Int) { foo(\$input) }", request.query) assertEquals("FooQuery", request.operationName) assertEquals(mapOf("input" to 1), request.variables) + + val requestFastJson = input.to() + assertTrue(requestFastJson is GraphQLRequest) + assertEquals("query FooQuery(\$input: Int) { foo(\$input) }", requestFastJson.query) + assertEquals("FooQuery", requestFastJson.operationName) + assertEquals(mapOf("input" to 1), requestFastJson.variables) } @Test @@ -116,15 +134,20 @@ class GraphQLServerRequestTest { val input = """[{"query":"query FooQuery(${'$'}input: Int) { foo(${'$'}input) }","operationName":"FooQuery","variables":{"input":1}},{"query":"query BarQuery { bar }"}]""" - val request = Json.decodeFromString(input) - + val request = mapper.readValue(input) assertTrue(request is GraphQLBatchRequest) assertEquals(2, request.requests.size) - val first = request.requests[0] - assertEquals("query FooQuery(\$input: Int) { foo(\$input) }", first.query) - assertEquals("FooQuery", first.operationName) - assertEquals(mapOf("input" to 1), first.variables) - val second = request.requests[1] - assertEquals("query BarQuery { bar }", second.query) + assertEquals("query FooQuery(\$input: Int) { foo(\$input) }", request.requests[0].query) + assertEquals("FooQuery", request.requests[0].operationName) + assertEquals(mapOf("input" to 1), request.requests[0].variables) + assertEquals("query BarQuery { bar }", request.requests[1].query) + + val requestFastJson = input.to() + assertTrue(requestFastJson is GraphQLBatchRequest) + assertEquals(2, requestFastJson.requests.size) + assertEquals("query FooQuery(\$input: Int) { foo(\$input) }", requestFastJson.requests[0].query) + assertEquals("FooQuery", requestFastJson.requests[0].operationName) + assertEquals(mapOf("input" to 1), requestFastJson.requests[0].variables) + assertEquals("query BarQuery { bar }", requestFastJson.requests[1].query) } } diff --git a/servers/graphql-kotlin-server/src/test/kotlin/com/expediagroup/graphql/server/types/GraphQLServerResponseTest.kt b/servers/graphql-kotlin-server/src/test/kotlin/com/expediagroup/graphql/server/types/GraphQLServerResponseTest.kt index be5bfd549d..b20dc9098b 100644 --- a/servers/graphql-kotlin-server/src/test/kotlin/com/expediagroup/graphql/server/types/GraphQLServerResponseTest.kt +++ b/servers/graphql-kotlin-server/src/test/kotlin/com/expediagroup/graphql/server/types/GraphQLServerResponseTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2024 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package com.expediagroup.graphql.server.types +import com.alibaba.fastjson2.JSON +import com.alibaba.fastjson2.JSONWriter import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue import org.junit.jupiter.api.Test @@ -26,6 +28,10 @@ import kotlin.test.assertTrue class GraphQLServerResponseTest { + init { + JSON.config(JSONWriter.Feature.WriteNulls) + } + class MyQuery(val foo: Int) private val mapper = jacksonObjectMapper() @@ -40,11 +46,12 @@ class GraphQLServerResponseTest { """{"data":{"foo":1}}""" assertEquals(expectedJson, mapper.writeValueAsString(response)) + assertEquals(expectedJson, JSON.toJSONString(response)) } @Test fun `verify complete serialization`() { - val request = GraphQLResponse( + val response = GraphQLResponse( data = MyQuery(1), errors = listOf(GraphQLServerError("my error")), extensions = mapOf("bar" to 2) @@ -53,7 +60,8 @@ class GraphQLServerResponseTest { val expectedJson = """{"data":{"foo":1},"errors":[{"message":"my error"}],"extensions":{"bar":2}}""" - assertEquals(expectedJson, mapper.writeValueAsString(request)) + assertEquals(expectedJson, mapper.writeValueAsString(response)) + assertEquals(expectedJson, JSON.toJSONString(response)) } @Test @@ -73,6 +81,7 @@ class GraphQLServerResponseTest { val expectedJson = """[{"data":{"foo":1}},{"data":{"foo":2},"errors":[{"message":"my error"}],"extensions":{"bar":2}}]""" assertEquals(expectedJson, mapper.writeValueAsString(batchResponse)) + assertEquals(expectedJson, JSON.toJSONString(batchResponse)) } @Test diff --git a/servers/graphql-kotlin-spring-server/build.gradle.kts b/servers/graphql-kotlin-spring-server/build.gradle.kts index 5cf021d236..1c9e2e5648 100644 --- a/servers/graphql-kotlin-spring-server/build.gradle.kts +++ b/servers/graphql-kotlin-spring-server/build.gradle.kts @@ -13,6 +13,7 @@ dependencies { api(libs.kotlinx.coroutines.jdk8) api(libs.kotlinx.coroutines.reactor) api(libs.reactor.extensions) + api(libs.fastjson2.spring) kapt(libs.spring.boot.config) testImplementation(libs.kotlinx.coroutines.test) testImplementation(libs.spring.boot.test) diff --git a/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/GraphQLAutoConfiguration.kt b/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/GraphQLAutoConfiguration.kt index 813969c37d..b9c7cac63a 100644 --- a/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/GraphQLAutoConfiguration.kt +++ b/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/GraphQLAutoConfiguration.kt @@ -25,6 +25,7 @@ import org.springframework.context.annotation.Import */ @Configuration @Import( + GraphQLServerCodecConfiguration::class, GraphQLRoutesConfiguration::class, SubscriptionAutoConfiguration::class, SdlRouteConfiguration::class, diff --git a/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/GraphQLConfigurationProperties.kt b/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/GraphQLConfigurationProperties.kt index 7fbbc90fdf..45a084d93d 100644 --- a/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/GraphQLConfigurationProperties.kt +++ b/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/GraphQLConfigurationProperties.kt @@ -30,6 +30,7 @@ data class GraphQLConfigurationProperties( val packages: List, /** Boolean flag indicating whether to print the schema after generator creates it */ val printSchema: Boolean = false, + val serializationLibrary: SerializationLibrary = SerializationLibrary.JACKSON, @NestedConfigurationProperty val federation: FederationConfigurationProperties = FederationConfigurationProperties(), @NestedConfigurationProperty @@ -167,3 +168,8 @@ enum class BatchingStrategy { LEVEL_DISPATCHED, SYNC_EXHAUSTION } + +enum class SerializationLibrary { + JACKSON, + FASTJSON +} diff --git a/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/GraphQLServerCodecConfiguration.kt b/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/GraphQLServerCodecConfiguration.kt new file mode 100644 index 0000000000..9e3161f1e7 --- /dev/null +++ b/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/GraphQLServerCodecConfiguration.kt @@ -0,0 +1,37 @@ +package com.expediagroup.graphql.server.spring + +import com.alibaba.fastjson2.JSONWriter +import com.alibaba.fastjson2.support.config.FastJsonConfig +import com.alibaba.fastjson2.support.spring6.http.codec.Fastjson2Decoder +import com.alibaba.fastjson2.support.spring6.http.codec.Fastjson2Encoder +import com.fasterxml.jackson.databind.ObjectMapper +import org.springframework.context.annotation.Configuration +import org.springframework.http.codec.ServerCodecConfigurer +import org.springframework.web.reactive.config.EnableWebFlux +import org.springframework.web.reactive.config.WebFluxConfigurer + +@Configuration +@EnableWebFlux +class GraphQLServerCodecConfiguration( + private val config: GraphQLConfigurationProperties, + private val objectMapper: ObjectMapper, +) : WebFluxConfigurer { + override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) { + if (config.serializationLibrary == SerializationLibrary.FASTJSON) { + configurer.defaultCodecs().apply { + jackson2JsonDecoder(Fastjson2Decoder(objectMapper)) + jackson2JsonEncoder( + Fastjson2Encoder( + objectMapper, + FastJsonConfig().also { + it.setWriterFeatures( + JSONWriter.Feature.LargeObject, + JSONWriter.Feature.WriteNulls, + ) + }, + ) + ) + } + } + } +} diff --git a/servers/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/server/spring/SubscriptionConfigurationTest.kt b/servers/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/server/spring/SubscriptionConfigurationTest.kt index 73e42ac68a..acc93cce42 100644 --- a/servers/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/server/spring/SubscriptionConfigurationTest.kt +++ b/servers/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/server/spring/SubscriptionConfigurationTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023 Expedia, Inc + * Copyright 2024 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import graphql.GraphQL import graphql.schema.GraphQLSchema +import io.mockk.every import io.mockk.mockk import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test @@ -36,7 +37,6 @@ import org.springframework.boot.autoconfigure.AutoConfigurations import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration -import org.springframework.web.reactive.HandlerMapping import org.springframework.web.reactive.socket.server.support.WebSocketHandlerAdapter import reactor.core.publisher.Flux import java.time.Duration @@ -73,7 +73,7 @@ class SubscriptionConfigurationTest { assertThat(ctx).hasSingleBean(SubscriptionWebSocketHandler::class.java) assertThat(ctx).hasSingleBean(WebSocketHandlerAdapter::class.java) - assertThat(ctx).hasSingleBean(HandlerMapping::class.java) + assertThat(ctx).hasBean("subscriptionHandlerMapping") } } @@ -102,10 +102,9 @@ class SubscriptionConfigurationTest { assertThat(ctx).hasSingleBean(ApolloSubscriptionWebSocketHandler::class.java) - assertThat(ctx).hasSingleBean(WebSocketHandlerAdapter::class.java) - assertThat(ctx).getBean(WebSocketHandlerAdapter::class.java) + assertThat(ctx).hasBean("webSocketHandlerAdapter") + assertThat(ctx).getBean("webSocketHandlerAdapter") .isSameAs(customConfiguration.webSocketHandlerAdapter()) - assertThat(ctx).hasSingleBean(HandlerMapping::class.java) } } @@ -146,7 +145,9 @@ class SubscriptionConfigurationTest { @Bean fun subscription(): Subscription = SimpleSubscription() @Bean - fun webSocketHandlerAdapter(): WebSocketHandlerAdapter = mockk() + fun webSocketHandlerAdapter(): WebSocketHandlerAdapter = mockk { + every { order } returns 1 + } } // GraphQL spec requires at least single query to be present as Query type is needed to run introspection queries