commercetools / commercetools-sdk-java-v2

The e-commerce SDK from commercetools for Java.
https://commercetools.github.io/commercetools-sdk-java-v2/javadoc/index.html
Apache License 2.0
34 stars 15 forks source link

Deserialization to type BaseMoney fails #498

Open Sectan opened 11 months ago

Sectan commented 11 months ago

Describe the bug Querying price information of a product with the graphql-api results in deserialization errors:

Caused by: com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve subtype of [simple type, class com.commercetools.graphql.api.types.BaseMoney]: missing type id property '__typename' (for POJO property 'value')

To Reproduce

public class Reproducer {

    public static void main(String[] args) {
        final ProjectApiRoot client = createApiClient();
        Logger logger = LoggerFactory.getLogger(Reproducer.class.getName());

        GraphQLResponse<ProductQueryResult> responseEntity =
                client
                        .graphql()
                        .query(GraphQL.products(q -> q.limit(8))
                                .projection(p -> p.total().results().id().masterData().current().masterVariant().prices().value().centAmount()))
                        .executeBlocking()
                        .getBody();
        responseEntity.getData().getResults().forEach(result -> {
            logger.info("Id: " + result.getId() + "Name: " + result.getMasterData().getCurrent().getName());
            logger.info("Id: " + result.getId() + "Name: " + result.getMasterData().getCurrent().getMasterVariant().getPrices());
        });
        client.close();
    }

    public static ProjectApiRoot createApiClient() {
        var projectApiRoot = ApiRootBuilder.of()
                .defaultClient(
                        ClientCredentials.of()
                                .withClientId("<CLIENT-ID>")
                                .withClientSecret("<SECRET")
                                .build(),
                        ServiceRegion.GCP_EUROPE_WEST1.getOAuthTokenUrl(),
                        ServiceRegion.GCP_EUROPE_WEST1.getApiUrl()
                )
                .build("<PROJECT-KEY>");

        return projectApiRoot;
    }
}

Expected behavior Reproducer code should run and price information should be deserialized without an error.

Screenshots/Code snippet Following error is thrown: Caused by: com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve subtype of [simple type, class com.commercetools.graphql.api.types.BaseMoney]: missing type id property '__typename' (for POJO property 'value')

Stack information (please complete the following information):

Additional context NA

jenschude commented 11 months ago

The reason for this is that the BaseMoney is an interface and it has two implementations Money and HighPrecisionMoney. The Deserializer can't distinguish them as you didn't "cast" the request to either money or highprecision money

projectRoot
        .graphql()
        .query(GraphQL.products(q -> q.limit(8))
                .projection(p -> p.total().results().id().masterData().current().masterVariant().prices().value().onMoney().centAmount()))
        .executeBlocking()
        .getBody();

Adding onMoney() to the projection will request the __typeName field so the ObjectMapper can deserialize correctly.

Another possibility is to declare a DefaultImplementation for the ObjectMapper. This can be done by creating a Mixin class

import com.commercetools.graphql.api.types.Money;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonTypeInfo(
        use = JsonTypeInfo.Id.NAME,
        include = JsonTypeInfo.As.PROPERTY,
        property = "__typename",
        defaultImpl = Money.class // use this implementation if no subtype implementation can be found
)
public interface BaseMoneyMixin {
}

Then create Jackson module in your application:

import com.commercetools.graphql.api.types.BaseMoney;
import com.fasterxml.jackson.databind.module.SimpleModule;

public class BaseMoneyModule extends SimpleModule {
    private static final long serialVersionUID = 0L;

    public BaseMoneyModule() {
        setMixInAnnotation(BaseMoney.class, BaseMoneyMixin.class);
    }
}

And registering a service locator entry in your resources folder (resources/META-INF/services/com.fasterxml.jackson.databind.module.SimpleModule):

com.commercetools.graphql.api.BaseMoneyModule

The ObjectMapper of the SDK will load this module and register it, so that in case no projection implementation was choosen it defaults to Money.