Parsing of PageInfo fails (using generated types)
Using the schema found here (also attached as txt) schema.txt I've generated a query API.
I make the following query (generated by the query API):
query {
site {
settings {
url {
vanityUrl
}
}
products(first: 10) {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
entityId
name
plainTextDescription(characterLimit: 1024)
brand {
name
}
availabilityV2 {
status
}
inventory {
hasVariantInventory
aggregated {
availableToSell
}
}
path
categories(first: 10) {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
name
}
}
}
images(first: 50) {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
urlOriginal
altText
}
}
}
prices {
price {
value
currencyCode
}
basePrice {
value
currencyCode
}
salePrice {
value
currencyCode
}
}
productOptions(first: 3) {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
displayName
... on MultipleChoiceOption {
__typename
values {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
label
}
}
}
}
}
}
}
variants(first: 200) {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
inventory {
aggregated {
availableToSell
}
}
entityId
productOptions(first: 3) {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
entityId
displayName
... on MultipleChoiceOption {
__typename
values {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
label
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
I get this response back:
{
"data" : {
"site" : {
"settings" : {
"url" : {
"vanityUrl" : "https://shop.soapboxus.com"
}
},
"products" : {
"pageInfo" : {
"hasNextPage" : false,
"endCursor" : "YXJyYXljb25uZWN0aW9uOjA="
},
"edges" : [ {
"node" : {
"entityId" : 175,
"name" : "Tide ReLoad Compatible Detergent",
"plainTextDescription" : "\n\nThis Box Contains\nOne Tide ReLoad™ tray and Detergent\nTide ReLoad™ tray works with multiple types of detergent, shipped directly from us with free shipping. Just choose the type of Tide you like best – Tide PODS Original, Tide PODS Spring Meadow, or Tide Eco-Box Original. Please note that Tide ReLoad™ tray will not work with Tide products purchased elsewhere.\n",
"brand" : null,
"availabilityV2" : {
"status" : "Available"
},
"inventory" : {
"hasVariantInventory" : false,
"aggregated" : null
},
"path" : "/tide-smart-tray-detergent/",
"categories" : {
"pageInfo" : {
"hasNextPage" : false,
"endCursor" : null
},
"edges" : [ ]
},
"images" : {
"pageInfo" : {
"hasNextPage" : false,
"endCursor" : "YXJyYXljb25uZWN0aW9uOjY="
},
"edges" : [ {
"node" : {
"urlOriginal" : "https://cdn11.bigcommerce.com/s-v1a6036467/images/stencil/original/products/175/800/Image_1_-_Delivered__03950.1633532989.jpg",
"altText" : ""
}
}, {
"node" : {
"urlOriginal" : "https://cdn11.bigcommerce.com/s-v1a6036467/images/stencil/original/products/175/802/Image_2_-_Free_Shipping__53500.1633532989.jpg",
"altText" : ""
}
}, {
"node" : {
"urlOriginal" : "https://cdn11.bigcommerce.com/s-v1a6036467/images/stencil/original/products/175/815/Image_3_-_Never_run_out_copy__63238.1633532989.jpg",
"altText" : ""
}
}, {
"node" : {
"urlOriginal" : "https://cdn11.bigcommerce.com/s-v1a6036467/images/stencil/original/products/175/799/Image_4_-_Cancel_Anytime__26398.1633532989.jpg",
"altText" : ""
}
}, {
"node" : {
"urlOriginal" : "https://cdn11.bigcommerce.com/s-v1a6036467/images/stencil/original/products/175/798/Image_5_-_Set_up__06965.1633532989.jpg",
"altText" : ""
}
}, {
"node" : {
"urlOriginal" : "https://cdn11.bigcommerce.com/s-v1a6036467/images/stencil/original/products/175/803/Image_6_-_Skip_the_trip_to_the_store__76617.1633532989.jpg",
"altText" : ""
}
}, {
"node" : {
"urlOriginal" : "https://cdn11.bigcommerce.com/s-v1a6036467/images/stencil/original/products/175/801/Image_7_-_Comes_with__54036.1633532989.jpg",
"altText" : ""
}
} ]
},
"prices" : {
"price" : {
"value" : 19.99,
"currencyCode" : "USD"
},
"basePrice" : {
"value" : 19.99,
"currencyCode" : "USD"
},
"salePrice" : null
},
"productOptions" : {
"pageInfo" : {
"hasNextPage" : false,
"endCursor" : "YXJyYXljb25uZWN0aW9uOjA="
},
"edges" : [ {
"node" : {
"displayName" : "Your detergent selection",
"__typename" : "MultipleChoiceOption",
"values" : {
"pageInfo" : {
"hasNextPage" : false,
"endCursor" : "YXJyYXljb25uZWN0aW9uOjI="
},
"edges" : [ {
"node" : {
"label" : "Original"
}
}, {
"node" : {
"label" : "Spring Meadow"
}
}, {
"node" : {
"label" : "Tide Eco Box"
}
} ]
}
}
} ]
},
"variants" : {
"pageInfo" : {
"hasNextPage" : false,
"endCursor" : "YXJyYXljb25uZWN0aW9uOjI="
},
"edges" : [ {
"node" : {
"inventory" : {
"aggregated" : null
},
"entityId" : 170,
"productOptions" : {
"pageInfo" : {
"hasNextPage" : false,
"endCursor" : "YXJyYXljb25uZWN0aW9uOjA="
},
"edges" : [ {
"node" : {
"entityId" : 21,
"displayName" : "Your detergent selection",
"__typename" : "MultipleChoiceOption",
"values" : {
"pageInfo" : {
"hasNextPage" : false,
"endCursor" : "YXJyYXljb25uZWN0aW9uOjA="
},
"edges" : [ {
"node" : {
"label" : "Original"
}
} ]
}
}
} ]
}
}
}, {
"node" : {
"inventory" : {
"aggregated" : null
},
"entityId" : 171,
"productOptions" : {
"pageInfo" : {
"hasNextPage" : false,
"endCursor" : "YXJyYXljb25uZWN0aW9uOjA="
},
"edges" : [ {
"node" : {
"entityId" : 21,
"displayName" : "Your detergent selection",
"__typename" : "MultipleChoiceOption",
"values" : {
"pageInfo" : {
"hasNextPage" : false,
"endCursor" : "YXJyYXljb25uZWN0aW9uOjA="
},
"edges" : [ {
"node" : {
"label" : "Spring Meadow"
}
} ]
}
}
} ]
}
}
}, {
"node" : {
"inventory" : {
"aggregated" : null
},
"entityId" : 172,
"productOptions" : {
"pageInfo" : {
"hasNextPage" : false,
"endCursor" : "YXJyYXljb25uZWN0aW9uOjA="
},
"edges" : [ {
"node" : {
"entityId" : 21,
"displayName" : "Your detergent selection",
"__typename" : "MultipleChoiceOption",
"values" : {
"pageInfo" : {
"hasNextPage" : false,
"endCursor" : "YXJyYXljb25uZWN0aW9uOjA="
},
"edges" : [ {
"node" : {
"label" : "Tide Eco Box"
}
} ]
}
}
} ]
}
}
} ]
}
}
} ]
}
}
}
}
Then when I attempt to parse the site object into a type generated by the codegen plugin...
Optional<JsonNode> dataOptional =
Optional.of(objectMapper.readTree(graphQLResponse.getJson()));
System.out.println(dataOptional.get().toPrettyString());
Optional<JsonNode> siteOptional =
dataOptional.map(json -> json.get("data")).map(data -> data.get("site"));
if (siteOptional.isEmpty()) {
throw new NullPointerException();
}
com.attentivemobile.syncprocessor.model.bigcommerce.types.Site site =
objectMapper.treeToValue(siteOptional.get(), Site.class);
I get this
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `graphql.relay.PageInfo` (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
at [Source: UNKNOWN; byte offset: #UNKNOWN] (through reference chain: com.attentivemobile.syncprocessor.model.bigcommerce.types.Site["products"]->com.attentivemobile.syncprocessor.model.bigcommerce.types.ProductConnection["pageInfo"])
at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)
at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1904)
at com.fasterxml.jackson.databind.DatabindContext.reportBadDefinition(DatabindContext.java:400)
at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1349)
at com.fasterxml.jackson.databind.deser.AbstractDeserializer.deserialize(AbstractDeserializer.java:274)
at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:129)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:313)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:176)
at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:129)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:313)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:176)
at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:322)
at com.fasterxml.jackson.databind.ObjectMapper._readValue(ObjectMapper.java:4650)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2831)
at com.fasterxml.jackson.databind.ObjectMapper.treeToValue(ObjectMapper.java:3295)
at com.attentivemobile.syncprocessor.service.vendor.bigcommerce.Test.test(Test.java:78)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:688)
at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
at ...
This is my codegen configuration, via the community maven port
<plugin>
<groupId>io.github.deweyjose</groupId>
<artifactId>graphqlcodegen-maven-plugin</artifactId>
<version>1.16</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<schemaPaths>
<param>
src/main/resources/api/bigcommerce/schema.graphql
</param>
</schemaPaths>
<packageName>com.attentivemobile.syncprocessor.model.bigcommerce</packageName>
<!-- Required because scalars chosen by BigCommerce impose graphql-java scalar mappings-->
<typeMapping>
<BigDecimal>java.math.BigDecimal</BigDecimal>
<Long>java.lang.Long</Long>
</typeMapping>
<generateClient>true</generateClient>
<includeQueries>site</includeQueries>
<includeSubscriptions>[]</includeSubscriptions>
<includeMutations>[]</includeMutations>
<omitNullInputFields>true</omitNullInputFields>
<maxProjectionDepth>20</maxProjectionDepth>
<outputDir>${project.build.directory}/generated-sources/graphqlcodegen</outputDir>
<kotlinAllFieldsOptional>true</kotlinAllFieldsOptional>
<language>java</language>
</configuration>
</execution>
</executions>
</plugin>
and these are the versions of the dgs libraries I'm using
<!-- https://mvnrepository.com/artifact/com.netflix.graphql.dgs.codegen/graphql-dgs-codegen-client-core -->
<dependency>
<groupId>com.netflix.graphql.dgs.codegen</groupId>
<artifactId>graphql-dgs-codegen-client-core</artifactId>
<version>5.1.16</version>
</dependency>
<dependency>
<groupId>com.netflix.graphql.dgs</groupId>
<artifactId>graphql-dgs-client</artifactId>
<version>4.9.20</version>
</dependency>
Jackson needs to be told explicitly what concrete type to deserialize to for interfaces and abstract types such as PageInfo and ConnectionCursor. This can be done by registering a custom module and calling addAbstractTypeMapping. Jackson would still have trouble deserializing to DefaultPageInfo, so you would need a ValueInstantiator or Deserializer to handle that.
import com.fasterxml.jackson.databind.DeserializationConfig
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JavaType
import com.fasterxml.jackson.databind.PropertyMetadata
import com.fasterxml.jackson.databind.PropertyName
import com.fasterxml.jackson.databind.deser.CreatorProperty
import com.fasterxml.jackson.databind.deser.SettableBeanProperty
import com.fasterxml.jackson.databind.deser.ValueInstantiator
import graphql.relay.ConnectionCursor
import graphql.relay.DefaultPageInfo
class DefaultPageInfoValueInstantiator : ValueInstantiator.Base(DefaultPageInfo::class.java) {
override fun canCreateFromObjectWith(): Boolean {
return true
}
override fun getFromObjectArguments(config: DeserializationConfig): Array<SettableBeanProperty> {
val connectionCursorType = config.constructType(ConnectionCursor::class.java)
val booleanType = config.constructType(Boolean::class.java)
return arrayOf(
creatorProp("startCursor", connectionCursorType, 0),
creatorProp("endCursor", connectionCursorType, 1),
creatorProp("hasPrevious", booleanType, 2),
creatorProp("hasNext", booleanType, 3)
)
}
override fun createFromObjectWith(ctxt: DeserializationContext, args: Array<out Any>): Any {
return DefaultPageInfo(args[0] as ConnectionCursor, args[1] as ConnectionCursor, args[2] as Boolean, args[3] as Boolean)
}
private fun creatorProp(name: String, type: JavaType, index: Int): CreatorProperty {
return CreatorProperty.construct(PropertyName.construct(name), type, null, null, null, null, index, null, PropertyMetadata.STD_OPTIONAL)
}
}
And the Jackson module would look like this:
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer
import graphql.relay.ConnectionCursor
import graphql.relay.DefaultConnectionCursor
import graphql.relay.DefaultPageInfo
import graphql.relay.PageInfo
class GraphQLModule : SimpleModule() {
init {
addAbstractTypeMapping(PageInfo::class.java, DefaultPageInfo::class.java)
addAbstractTypeMapping(ConnectionCursor::class.java, DefaultConnectionCursor::class.java)
addSerializer(DefaultConnectionCursor::class.java, ToStringSerializer.instance)
addValueInstantiator(DefaultPageInfo::class.java, DefaultPageInfoValueInstantiator())
}
}
Example usage:
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import graphql.relay.PageInfo
data class Response(val pageInfo: PageInfo)
val objectMapper = jacksonObjectMapper()
.registerModule(GraphQLModule())
val response = objectMapper.readValue<Response>("""{"pageInfo": {"endCursor": "bar", "hasPrevious": true, "hasNext": true}}""")
println(response)
// prints Response(pageInfo=DefaultPageInfo{ startCursor=null, endCursor=bar, hasPreviousPage=true, hasNextPage=true})
println("json = " + objectMapper.writeValueAsString(response))
// print json = {"pageInfo":{"startCursor":null,"endCursor":"bar","hasPreviousPage":true,"hasNextPage":true}}
I have actually run into the same issue, particularly when writing tests for server-side responses. Now whether this is something that belongs in dgs-codegen (or elsewhere?), I am not sure. @berngp thoughts?
Thanks @kilink. I think it makes sense to handle this in dgs-codegen since it is a well known and supported type.
On Apr 14, 2022, at 11:12 AM, Patrick Strawderman @.***> wrote:
Jackson needs to be told explicitly what concrete type to deserialize to for interfaces and abstract types such as PageInfo and ConnectionCursor. This can be done by registering a custom module and calling addAbstractTypeMapping. Jackson would still have trouble deserializing to DefaultPageInfo, so you would need a ValueInstantiator or Deserializer to handle that.
import com.fasterxml.jackson.databind.DeserializationConfig import com.fasterxml.jackson.databind.DeserializationContext import com.fasterxml.jackson.databind.JavaType import com.fasterxml.jackson.databind.PropertyMetadata import com.fasterxml.jackson.databind.PropertyName import com.fasterxml.jackson.databind.deser.CreatorProperty import com.fasterxml.jackson.databind.deser.SettableBeanProperty import com.fasterxml.jackson.databind.deser.ValueInstantiator import graphql.relay.ConnectionCursor import graphql.relay.DefaultPageInfo
class DefaultPageInfoValueInstantiator : ValueInstantiator.Base(DefaultPageInfo::class.java) { override fun canCreateFromObjectWith(): Boolean { return true }
override fun getFromObjectArguments(config: DeserializationConfig): Array<SettableBeanProperty> { val connectionCursorType = config.constructType(ConnectionCursor::class.java) val booleanType = config.constructType(Boolean::class.java) return arrayOf( creatorProp("startCursor", connectionCursorType, 0), creatorProp("endCursor", connectionCursorType, 1), creatorProp("hasPrevious", booleanType, 2), creatorProp("hasNext", booleanType, 3) ) } override fun createFromObjectWith(ctxt: DeserializationContext, args: Array<out Any>): Any { return DefaultPageInfo(args[0] as ConnectionCursor, args[1] as ConnectionCursor, args[2] as Boolean, args[3] as Boolean) } private fun creatorProp(name: String, type: JavaType, index: Int): CreatorProperty { return CreatorProperty.construct(PropertyName.construct(name), type, null, null, null, null, index, null, PropertyMetadata.STD_OPTIONAL) }} And the Jackson module would look like this:
import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.databind.ser.std.ToStringSerializer import graphql.relay.ConnectionCursor import graphql.relay.DefaultConnectionCursor import graphql.relay.DefaultPageInfo import graphql.relay.PageInfo
class GraphQLModule : SimpleModule() { init { addAbstractTypeMapping(PageInfo::class.java, DefaultPageInfo::class.java) addAbstractTypeMapping(ConnectionCursor::class.java, DefaultConnectionCursor::class.java)
addSerializer(DefaultConnectionCursor::class.java, ToStringSerializer.instance) addValueInstantiator(DefaultPageInfo::class.java, DefaultPageInfoValueInstantiator()) }} Example usage:
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue import graphql.relay.PageInfo
data class Response(val pageInfo: PageInfo)
val objectMapper = jacksonObjectMapper() .registerModule(GraphQLModule())
val response = objectMapper.readValue<Response>("""{"pageInfo": {"endCursor": "bar", "hasPrevious": true, "hasNext": true}}""") println(response) // prints Response(pageInfo=DefaultPageInfo{ startCursor=null, endCursor=bar, hasPreviousPage=true, hasNextPage=true}) println("json = " + objectMapper.writeValueAsString(response)) // print json = {"pageInfo":{"startCursor":null,"endCursor":"bar","hasPreviousPage":true,"hasNextPage":true}} I have actually run into the same issue, particularly when writing tests for server-side responses. Now whether this is something that belongs in dgs-codegen (or elsewhere?), I am not sure. @berngp thoughts?
— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you are subscribed to this thread.