Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,21 @@ import com.expediagroup.graphql.client.jackson.types.UndefinedFilter
import com.expediagroup.graphql.client.serializer.GraphQLClientSerializer
import com.expediagroup.graphql.client.types.GraphQLClientRequest
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.JavaType
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import tools.jackson.databind.JavaType
import tools.jackson.databind.ObjectMapper
import tools.jackson.databind.cfg.EnumFeature
import tools.jackson.databind.json.JsonMapper
import tools.jackson.module.kotlin.jacksonMapperBuilder
import java.util.concurrent.ConcurrentHashMap
import kotlin.reflect.KClass

/**
* Jackson based GraphQL request/response serializer.
*/
class GraphQLClientJacksonSerializer(private val mapper: ObjectMapper = jacksonObjectMapper()) : GraphQLClientSerializer {
class GraphQLClientJacksonSerializer(mapper: JsonMapper = jacksonMapperBuilder().build()) : GraphQLClientSerializer {
private val mapper: ObjectMapper = configureMapper(mapper)
private val typeCache = ConcurrentHashMap<KClass<*>, JavaType>()

init {
mapper.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL)
mapper.configOverride(OptionalInput::class.java).include = JsonInclude.Value.empty().withValueInclusion(JsonInclude.Include.CUSTOM).withValueFilter(UndefinedFilter::class.java)
}

override fun serialize(request: GraphQLClientRequest<*>): String = mapper.writeValueAsString(request)

override fun serialize(requests: List<GraphQLClientRequest<*>>): String = mapper.writeValueAsString(requests)
Expand All @@ -66,4 +62,18 @@ class GraphQLClientJacksonSerializer(private val mapper: ObjectMapper = jacksonO
typeCache.computeIfAbsent(resultType) {
mapper.typeFactory.constructParametricType(JacksonGraphQLResponse::class.java, resultType.java)
}

companion object {
private fun configureMapper(mapper: JsonMapper): JsonMapper = mapper.rebuild()
.enable(EnumFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)
.changeDefaultPropertyInclusion { it.withValueInclusion(JsonInclude.Include.NON_NULL) }
.withConfigOverride(OptionalInput::class.java) { cfg ->
cfg.setInclude(
JsonInclude.Value.empty()
.withValueInclusion(JsonInclude.Include.CUSTOM)
.withValueFilter(UndefinedFilter::class.java)
)
}
.build()
Comment on lines +66 to +77
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ObjectMapper objects are now immutable, so we need to rebuild it to apply the settings that we need.

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,24 @@
package com.expediagroup.graphql.client.jackson.serializers

import com.expediagroup.graphql.client.jackson.types.OptionalInput
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.SerializerProvider
import tools.jackson.core.JsonGenerator
import tools.jackson.databind.SerializationContext
import tools.jackson.databind.ValueSerializer

class OptionalInputSerializer : JsonSerializer<OptionalInput<*>>() {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

class OptionalInputSerializer : ValueSerializer<OptionalInput<*>>() {

override fun isEmpty(provider: SerializerProvider, value: OptionalInput<*>?): Boolean {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed, and the default name of the parameter is ctxt so I changed it to align: https://github.com/FasterXML/jackson-databind/blob/3.x/src/main/java/tools/jackson/databind/ValueSerializer.java#L223

override fun isEmpty(ctxt: SerializationContext, value: OptionalInput<*>?): Boolean {
return value == OptionalInput.Undefined
}

override fun serialize(value: OptionalInput<*>, gen: JsonGenerator, serializers: SerializerProvider) {
override fun serialize(value: OptionalInput<*>, gen: JsonGenerator, ctxt: SerializationContext) {
when (value) {
is OptionalInput.Undefined -> return
is OptionalInput.Defined -> {
if (value.value == null) {
serializers.defaultNullValueSerializer.serialize(value.value, gen, serializers)
ctxt.defaultNullValueSerializer.serialize(value.value, gen, ctxt)
} else {
serializers.findValueSerializer(value.value::class.java).serialize(value.value, gen, serializers)
ctxt.findValueSerializer(value.value::class.java).serialize(value.value, gen, ctxt)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ package com.expediagroup.graphql.client.jackson.types
import com.expediagroup.graphql.client.jackson.serializers.OptionalInputSerializer
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonValue
import com.fasterxml.jackson.databind.annotation.JsonSerialize
import tools.jackson.databind.annotation.JsonSerialize

@JsonSerialize(using = OptionalInputSerializer::class)
sealed class OptionalInput<out T> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,22 @@ import com.expediagroup.graphql.client.jackson.types.JacksonGraphQLError
import com.expediagroup.graphql.client.jackson.types.JacksonGraphQLResponse
import com.expediagroup.graphql.client.jackson.types.JacksonGraphQLSourceLocation
import com.expediagroup.graphql.client.jackson.types.OptionalInput
import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.junit.jupiter.api.Test
import tools.jackson.databind.SerializationFeature
import tools.jackson.module.kotlin.jacksonMapperBuilder
import java.util.UUID
import kotlin.test.assertEquals

class GraphQLClientJacksonSerializerTest {

private val testMapper = jacksonObjectMapper()
.enable(SerializationFeature.INDENT_OUTPUT)
private val testMapper = jacksonMapperBuilder().enable(SerializationFeature.INDENT_OUTPUT).build()
private val serializer = GraphQLClientJacksonSerializer(testMapper)

private fun assertSerializedJsonEquals(expected: String, actual: String) {
// Check the contents rather than the string order
assertEquals(testMapper.readTree(expected), testMapper.readTree(actual))
}

Comment on lines +46 to +50
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default ordering of the generated serialized objects changed https://github.com/FasterXML/jackson/blob/main/jackson3/MIGRATING_TO_JACKSON_3.md#changes-mapperfeature

This test previously checked for exact equivalence, including ordering. It's sufficient to just check for functional equivalence.

@Test
fun `verify we can serialize GraphQLClientRequest`() {
val testQuery = FirstQuery(FirstQuery.Variables(input = 1.0f))
Expand All @@ -58,7 +62,7 @@ class GraphQLClientJacksonSerializerTest {
""".trimMargin()

val serialized = serializer.serialize(testQuery)
assertEquals(expected, serialized)
assertSerializedJsonEquals(expected, serialized)
}

@Test
Expand All @@ -78,7 +82,7 @@ class GraphQLClientJacksonSerializerTest {
""".trimMargin()

val serialized = serializer.serialize(queries)
assertEquals(expected, serialized)
assertSerializedJsonEquals(expected, serialized)
}

@Test
Expand Down Expand Up @@ -193,7 +197,7 @@ class GraphQLClientJacksonSerializerTest {
""".trimMargin()

val serialized = serializer.serialize(scalarQuery)
assertEquals(expected, serialized)
assertSerializedJsonEquals(expected, serialized)
}

@Test
Expand Down Expand Up @@ -239,7 +243,7 @@ class GraphQLClientJacksonSerializerTest {
""".trimMargin()

val serialized = serializer.serialize(query)
assertEquals(expected, serialized)
assertSerializedJsonEquals(expected, serialized)
}

@Test
Expand Down Expand Up @@ -276,7 +280,7 @@ class GraphQLClientJacksonSerializerTest {
""".trimMargin()

val serialized = serializer.serialize(query)
assertEquals(expected, serialized)
assertSerializedJsonEquals(expected, serialized)
}

@Test
Expand Down Expand Up @@ -309,7 +313,7 @@ class GraphQLClientJacksonSerializerTest {
|}
""".trimMargin()
val serialized = serializer.serialize(query)
assertEquals(expected, serialized)
assertSerializedJsonEquals(expected, serialized)
}

@Test
Expand All @@ -325,7 +329,7 @@ class GraphQLClientJacksonSerializerTest {
|}
""".trimMargin()
val serialized = serializer.serialize(query)
assertEquals(expected, serialized)
assertSerializedJsonEquals(expected, serialized)
}

@Test
Expand All @@ -345,6 +349,6 @@ class GraphQLClientJacksonSerializerTest {
""".trimMargin()

val serialized = serializer.serialize(entitiesQuery)
assertEquals(expected, serialized)
assertSerializedJsonEquals(expected, serialized)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import com.expediagroup.graphql.client.jackson.data.entitiesquery._Entity
import com.expediagroup.graphql.client.jackson.data.scalars.AnyToAnyConverter
import com.expediagroup.graphql.client.types.GraphQLClientRequest
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.annotation.JsonSerialize
import tools.jackson.databind.annotation.JsonDeserialize
import tools.jackson.databind.annotation.JsonSerialize
import kotlin.reflect.KClass

class EntitiesQuery(
Expand All @@ -34,8 +34,8 @@ class EntitiesQuery(
override fun responseType(): KClass<Result> = Result::class

data class Variables(
@JsonSerialize(contentConverter = AnyToAnyConverter::class)
@JsonDeserialize(contentConverter = AnyToAnyConverter::class)
@get:JsonSerialize(contentConverter = AnyToAnyConverter::class)
@get:JsonDeserialize(contentConverter = AnyToAnyConverter::class)
@get:JsonProperty("representations")
public val representations: List<Any>,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import com.expediagroup.graphql.client.jackson.data.scalars.AnyToUUIDConverter
import com.expediagroup.graphql.client.jackson.data.scalars.UUIDToAnyConverter
import com.expediagroup.graphql.client.types.GraphQLClientRequest
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.annotation.JsonSerialize
import tools.jackson.databind.annotation.JsonDeserialize
import tools.jackson.databind.annotation.JsonSerialize
import java.util.UUID
import kotlin.reflect.KClass

Expand All @@ -40,16 +40,16 @@ class ScalarQuery(
data class Variables(
@get:JsonProperty("alias")
val alias: ID? = null,
@JsonSerialize(converter = UUIDToAnyConverter::class)
@JsonDeserialize(converter = AnyToUUIDConverter::class)
@get:JsonSerialize(converter = UUIDToAnyConverter::class)
@get:JsonDeserialize(converter = AnyToUUIDConverter::class)
@get:JsonProperty("custom")
val custom: UUID? = null
Comment thread
JordanJLopez marked this conversation as resolved.
)

data class Result(
val scalarAlias: ID,
@JsonSerialize(converter = UUIDToAnyConverter::class)
@JsonDeserialize(converter = AnyToUUIDConverter::class)
@get:JsonSerialize(converter = UUIDToAnyConverter::class)
@get:JsonDeserialize(converter = AnyToUUIDConverter::class)
val customScalar: UUID
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
package com.expediagroup.graphql.client.jackson.data.scalars

import com.expediagroup.graphql.client.converter.ScalarConverter
import com.fasterxml.jackson.databind.util.StdConverter
import com.fasterxml.jackson.annotation.JsonProperty
import tools.jackson.databind.util.StdConverter
import kotlin.Any

class AnyToAnyConverter : StdConverter<Any, Any>() {
Expand All @@ -34,5 +35,6 @@ class AnyScalarConverter : ScalarConverter<Any> {

// representation would not be part of the generated sources
data class ProductEntityRepresentation(val id: String) {
@get:JsonProperty("__typename")
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As part of the upgrade, this explicit annotation is now required. Previously it would be fine for it to be implicit.

val __typename: String = "Product"
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
package com.expediagroup.graphql.client.jackson.data.scalars

import com.expediagroup.graphql.client.converter.ScalarConverter
import com.fasterxml.jackson.databind.util.StdConverter
import tools.jackson.databind.util.StdConverter
import java.util.UUID
import kotlin.Any

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,15 @@ package com.expediagroup.graphql.client.jackson.serializers

import com.expediagroup.graphql.client.jackson.types.OptionalInput
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.junit.jupiter.api.Test
import tools.jackson.module.kotlin.jacksonMapperBuilder
import kotlin.test.assertEquals

class OptionalInputSerializerTest {

private val mapper = jacksonObjectMapper()
init {
mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY)
}
private val mapper = jacksonMapperBuilder()
.changeDefaultPropertyInclusion { incl -> incl.withValueInclusion(JsonInclude.Include.NON_EMPTY) }
.build()
Comment on lines +27 to +29
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This demonstrates the new builder pattern


@Test
fun `verify undefined value is serialized to empty JSON`() {
Expand Down
Loading
Loading