Netflix / dgs-codegen

Apache License 2.0
177 stars 92 forks source link

Polymorphic Interfaces with default implementation #689

Closed jenschude closed 1 month ago

jenschude commented 2 months ago

The code generator supports polymorphic interfaces like the one below:

scalar Currency
scalar Long

interface BaseMoney {
  type: String!
  currencyCode: Currency!
  centAmount: Long!
}

type HighPrecisionMoney implements BaseMoney {
  type: String!
  currencyCode: Currency!
  preciseAmount: Long!
  centAmount: Long!
  fractionDigits: Int!
}

type Money implements BaseMoney {
  type: String!
  currencyCode: Currency!
  centAmount: Long!
  fractionDigits: Int!
}

type ProductPrice {
  id: String
  value: BaseMoney!
  key: String
}

Using the generator is creates all the necessary types and query/projection classes. But there is one drawback, when only using fields from the interface you still have to do a projection against one of the subtypes, cause the interface doesn't define a default implementation: .projection(p -> p.value().onMoney().centAmount()). The onMoney method in this case creates a projection query like this:

{
   price {
     value {
       ...on Money {
          __typeName
          centAmount
       }
     }
}

The __typeName field here is important to be able to correctly deserialize the polymorphic type:

@JsonTypeInfo(
    use = JsonTypeInfo.Id.NAME,
    include = JsonTypeInfo.As.PROPERTY,
    property = "__typename"
)
@JsonSubTypes({
    @JsonSubTypes.Type(value = HighPrecisionMoney.class, name = "HighPrecisionMoney"),
    @JsonSubTypes.Type(value = Money.class, name = "Money")
})
public interface BaseMoney {

Without the call to the on-method the __typeName field will not be queried and the deserializer fails with

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')

Adding the attribute defaultImpl to the BaseMoneys JsonTypeInfo and generating a defaultImpl class sufficient for the interface this would save the necessary call to one of the on methods in the projection .projection(p -> p.value().centAmount()) cause now the deserializer knows which class to instantiate in case there is no discriminator property available in the response.

@JsonTypeInfo(
    use = JsonTypeInfo.Id.NAME,
    include = JsonTypeInfo.As.PROPERTY,
    property = "__typename",
        defaultImpl = BaseMoneyImpl.class // <--- add this line
)
@JsonSubTypes({
    @JsonSubTypes.Type(value = HighPrecisionMoney.class, name = "HighPrecisionMoney"),
    @JsonSubTypes.Type(value = Money.class, name = "Money")
})
public interface BaseMoney {

Generate additionally this class as default implementation of the interface

@JsonTypeInfo(
    use = JsonTypeInfo.Id.NONE
)
public class BaseMoneyImpl implements BaseMoney {
srinivasankavitha commented 2 months ago

For implementations of interfaces, the typename should be automatically added. In any case, we recently added the feature to request __typename on the interface type as well : v6.1.10. Please give that a try for the typename.

jenschude commented 1 month ago

Yes this already helps. But it's still an explicit call to be able to deserialize it to one of the subtypes.

When projecting fields of the parent type only it should be sufficient to deserialize to this type as default.

srinivasankavitha commented 1 month ago

Yes you would need to use the OnImplementationType to get the subtypes along with the __typename field.