From 62252fa5d65efa760c9303b982e54723a37b4d9d Mon Sep 17 00:00:00 2001 From: Samuel Vazquez Date: Fri, 4 Oct 2024 18:23:07 -0700 Subject: [PATCH] feat(v6): fastjson2 for serialization of GraphQLResponse and deserialization of GraphQLRequest (#2043) ### :pencil: Description cherry pick https://github.com/ExpediaGroup/graphql-kotlin/pull/2040/files --------- Co-authored-by: Samuel Vazquez --- gradle.properties | 2 +- .../graphql-kotlin-server/build.gradle.kts | 15 +- ...verRequestBatchDeserializationBenchmark.kt | 64 ++++++++ ...QLServerRequestDeserializationBenchmark.kt | 31 +--- ...phQLServerRequestSerializationBenchmark.kt | 70 -------- ...rverResponseBatchSerializationBenchmark.kt | 63 +++++++ ...LServerResponseDeserializationBenchmark.kt | 66 -------- ...hQLServerResponseSerializationBenchmark.kt | 31 +--- .../kotlin/testtypes/GraphQLServerRequest.kt | 118 ------------- .../kotlin/testtypes/GraphQLServerResponse.kt | 155 ------------------ .../server/extensions/jsonReaderExtensions.kt | 10 ++ .../server/types/GraphQLServerError.kt | 5 +- .../server/types/GraphQLServerRequest.kt | 69 +++----- .../server/types/GraphQLServerResponse.kt | 5 +- .../serializers/AnyNullableKSerializer.kt | 74 --------- .../FastJsonIncludeNonNullProperty.kt | 11 ++ .../server/types/GraphQLServerRequestTest.kt | 61 ++++--- .../server/types/GraphQLServerResponseTest.kt | 15 +- .../build.gradle.kts | 4 +- .../server/spring/GraphQLAutoConfiguration.kt | 1 + .../spring/GraphQLConfigurationProperties.kt | 8 +- .../spring/GraphQLServerCodecConfiguration.kt | 52 ++++++ .../spring/SubscriptionConfigurationTest.kt | 14 +- 23 files changed, 333 insertions(+), 611 deletions(-) create mode 100644 servers/graphql-kotlin-server/src/benchmarks/kotlin/GraphQLServerRequestBatchDeserializationBenchmark.kt delete mode 100644 servers/graphql-kotlin-server/src/benchmarks/kotlin/GraphQLServerRequestSerializationBenchmark.kt create mode 100644 servers/graphql-kotlin-server/src/benchmarks/kotlin/GraphQLServerResponseBatchSerializationBenchmark.kt delete mode 100644 servers/graphql-kotlin-server/src/benchmarks/kotlin/GraphQLServerResponseDeserializationBenchmark.kt delete mode 100644 servers/graphql-kotlin-server/src/benchmarks/kotlin/testtypes/GraphQLServerRequest.kt delete mode 100644 servers/graphql-kotlin-server/src/benchmarks/kotlin/testtypes/GraphQLServerResponse.kt create mode 100644 servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/extensions/jsonReaderExtensions.kt delete mode 100644 servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/types/serializers/AnyNullableKSerializer.kt create mode 100644 servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/types/serializers/FastJsonIncludeNonNullProperty.kt create mode 100644 servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/GraphQLServerCodecConfiguration.kt diff --git a/gradle.properties b/gradle.properties index 8a6ed27636..b3010753e8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -34,7 +34,7 @@ reactorExtensionsVersion = 1.1.7 slf4jVersion = 1.7.36 springBootVersion = 2.7.2 springVersion = 5.3.22 - +fastjson2Version = 2.0.48 # test dependency versions # kotlin-compile-testing has to be using the same kotlin version as the kotlinx-serialization compiler # kotlin-compile-testing v1.4.9+ requires Kotlin v1.7 diff --git a/servers/graphql-kotlin-server/build.gradle.kts b/servers/graphql-kotlin-server/build.gradle.kts index be6da52fdd..d9128e1077 100644 --- a/servers/graphql-kotlin-server/build.gradle.kts +++ b/servers/graphql-kotlin-server/build.gradle.kts @@ -4,20 +4,19 @@ description = "Common code for running a GraphQL server in any HTTP server frame val kotlinCoroutinesVersion: String by project val kotlinxBenchmarkVersion: String by project +val jacksonVersion: String by project +val fastjson2Version: String by project plugins { id("org.jetbrains.kotlinx.benchmark") - kotlin("plugin.serialization") } -val jacksonVersion: String by project -val kotlinxSerializationVersion: String by project dependencies { api(project(path = ":graphql-kotlin-schema-generator")) api(project(path = ":graphql-kotlin-dataloader-instrumentation")) api(project(path = ":graphql-kotlin-automatic-persisted-queries")) api("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion") - api("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxSerializationVersion") + api("com.alibaba.fastjson2:fastjson2-kotlin:$fastjson2Version") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutinesVersion") } @@ -35,6 +34,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 054f582b61..fb852b4885 100644 --- a/servers/graphql-kotlin-server/src/benchmarks/kotlin/GraphQLServerRequestDeserializationBenchmark.kt +++ b/servers/graphql-kotlin-server/src/benchmarks/kotlin/GraphQLServerRequestDeserializationBenchmark.kt @@ -16,11 +16,12 @@ package com.expediagroup.graphql.server -import com.expediagroup.graphql.server.testtypes.GraphQLServerRequest -import com.expediagroup.graphql.server.testtypes.GraphQLServerRequestKSerializer +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 @@ -31,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() @@ -51,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(GraphQLServerRequestKSerializer, request) - - @Benchmark - fun KSerializationDeserializeGraphQLBatchRequest(): GraphQLServerRequest = Json.decodeFromString(GraphQLServerRequestKSerializer, 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/GraphQLServerResponseBatchSerializationBenchmark.kt b/servers/graphql-kotlin-server/src/benchmarks/kotlin/GraphQLServerResponseBatchSerializationBenchmark.kt new file mode 100644 index 0000000000..23b63a4aef --- /dev/null +++ b/servers/graphql-kotlin-server/src/benchmarks/kotlin/GraphQLServerResponseBatchSerializationBenchmark.kt @@ -0,0 +1,63 @@ +/* + * 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.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 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 GraphQLServerResponseBatchSerializationBenchmark { + private val mapper = jacksonObjectMapper() + private lateinit var batchResponse: GraphQLBatchResponse + + @Setup + fun setUp() { + 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 JacksonSerializeGraphQLBatchResponse(): String = mapper.writeValueAsString(batchResponse) + + @Benchmark + fun FastJsonSerializeGraphQLBatchResponse(): String = JSON.toJSONString(batchResponse) +} diff --git a/servers/graphql-kotlin-server/src/benchmarks/kotlin/GraphQLServerResponseDeserializationBenchmark.kt b/servers/graphql-kotlin-server/src/benchmarks/kotlin/GraphQLServerResponseDeserializationBenchmark.kt deleted file mode 100644 index e37bdd2a82..0000000000 --- a/servers/graphql-kotlin-server/src/benchmarks/kotlin/GraphQLServerResponseDeserializationBenchmark.kt +++ /dev/null @@ -1,66 +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.GraphQLServerResponse -import com.expediagroup.graphql.server.testtypes.GraphQLServerResponseKSerializer -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 -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 GraphQLServerResponseDeserializationBenchmark { - private val mapper = jacksonObjectMapper() - private lateinit var response: String - private lateinit var batchResponse: String - - @Setup - fun setUp() { - response = this::class.java.classLoader.getResource("StarWarsDetailsResponse.json")!!.readText() - batchResponse = """ - [ - $response, - $response, - $response, - $response - ] - """.trimIndent() - } - - @Benchmark - fun JacksonDeserializeGraphQLResponse(): GraphQLServerResponse = mapper.readValue(response) - - @Benchmark - fun JacksonDeserializeGraphQLBatchResponse(): GraphQLServerResponse = mapper.readValue(batchResponse) - - @Benchmark - fun KSerializationDeserializeGraphQLResponse(): GraphQLServerResponse = Json.decodeFromString(GraphQLServerResponseKSerializer, response) - - @Benchmark - fun KSerializationDeserializeGraphQLBatchResponse(): GraphQLServerResponse = Json.decodeFromString(GraphQLServerResponseKSerializer, 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..a5b2f26298 --- /dev/null +++ b/servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/extensions/jsonReaderExtensions.kt @@ -0,0 +1,10 @@ +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..2bdddf8fca 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 @@ -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.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..48ec1584cf 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 @@ -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.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 099a6dbd79..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(GraphQLServerRequestKSerializer, 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(GraphQLServerRequestKSerializer, 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(GraphQLServerRequestKSerializer, 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 d80047c3d3..1a36838a77 100644 --- a/servers/graphql-kotlin-spring-server/build.gradle.kts +++ b/servers/graphql-kotlin-spring-server/build.gradle.kts @@ -9,6 +9,7 @@ val kotlinCoroutinesVersion: String by project val springBootVersion: String by project val reactorVersion: String by project val reactorExtensionsVersion: String by project +val fastjson2Version: String by project dependencies { api(project(path = ":graphql-kotlin-server")) @@ -17,6 +18,7 @@ dependencies { api("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$kotlinCoroutinesVersion") api("io.projectreactor.kotlin:reactor-kotlin-extensions:$reactorExtensionsVersion") api("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$kotlinCoroutinesVersion") + api("com.alibaba.fastjson2:fastjson2-extension-spring5:$fastjson2Version") kapt("org.springframework.boot:spring-boot-configuration-processor:$springBootVersion") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutinesVersion") testImplementation("org.springframework.boot:spring-boot-starter-test:$springBootVersion") @@ -30,7 +32,7 @@ tasks { limit { counter = "INSTRUCTION" value = "COVEREDRATIO" - minimum = "0.93".toBigDecimal() + minimum = "0.91".toBigDecimal() } limit { counter = "BRANCH" 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 00bdb3bd57..915f87c67e 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, PlaygroundRouteConfiguration::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 583818a9d9..de681d63b3 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 @@ -1,5 +1,5 @@ /* - * Copyright 2022 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. @@ -31,6 +31,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, val federation: FederationConfigurationProperties = FederationConfigurationProperties(), val subscriptions: SubscriptionConfigurationProperties = SubscriptionConfigurationProperties(), val playground: PlaygroundConfigurationProperties = PlaygroundConfigurationProperties(), @@ -132,4 +133,9 @@ data class GraphQLConfigurationProperties( /** Boolean flag to enable or disable Automatic Persisted Queries. */ val enabled: Boolean = false ) + + 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..885289804c --- /dev/null +++ b/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/GraphQLServerCodecConfiguration.kt @@ -0,0 +1,52 @@ +/* + * 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.spring +import com.alibaba.fastjson2.JSONWriter +import com.alibaba.fastjson2.support.config.FastJsonConfig +import com.alibaba.fastjson2.support.spring.http.codec.Fastjson2Decoder +import com.alibaba.fastjson2.support.spring.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 == GraphQLConfigurationProperties.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 92306e3e74..093acadb19 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 2022 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. @@ -36,7 +36,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 +72,7 @@ class SubscriptionConfigurationTest { assertThat(ctx).hasSingleBean(SpringGraphQLSubscriptionHandler::class.java) assertThat(ctx).hasSingleBean(WebSocketHandlerAdapter::class.java) - assertThat(ctx).hasSingleBean(HandlerMapping::class.java) + assertThat(ctx).hasBean("subscriptionHandlerMapping") } } @@ -97,10 +96,9 @@ class SubscriptionConfigurationTest { assertThat(ctx).getBean(SpringGraphQLSubscriptionHandler::class.java) .isSameAs(customConfiguration.subscriptionHandler()) - 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) } } @@ -142,7 +140,9 @@ class SubscriptionConfigurationTest { } @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