How to setup dynamic response types in OCC web services?

I recently configured dynamic response types in my project following instructions documented in the official SAP Commerce Help and would like to share today this experience with you, hoping to make yours smoother. If you are already very familiar with dynamic types or just want to know how to configure them, feel free to skip the 2 first introduction sections.

What is a dynamic response type?

The examples presented in the official SAP Commerce Help are well illustrating, what a dynamic response type is. In a nutshell, when you decide to create your own data transfer object by extending a standard one rather than enhancing the standard data transfer object, you are creating a dynamic response type.

To illustrate it better, let’s imagine the following scenario: your company sells software, hardware and related products. You decide to enhance the standard catalog data model by introducing a SoftwareProduct and HardwareProduct itemtypes extending the standard Product itemtype.

<itemtype code="SoftwareProduct" extends="Product" autocreate="true" generate="true"> <attributes> <attribute qualifier="license" type="java.lang.String"> <description>License applicable to the software product</description> <persistence type="property"/> </attribute> </attributes>
</itemtype> <itemtype code="HardwareProduct" extends="Product" autocreate="true" generate="true"> <attributes> <attribute qualifier="power" type="java.lang.Double"> <description>Watts required to power the product</description> <persistence type="property"/> </attribute> </attributes>
</itemtype>

You enhance as well the product facade implementation to expose these products respectively as SoftwareProductData and HardwareProductData objects extending the standard ProductData object.

<bean class="mypackage.SoftwareProductData" extends="de.hybris.platform.commercefacades.product.data.ProductData"> <property name="license" type="java.lang.String"/>
</bean> <bean class="mypackage.HardwareProductData" extends="de.hybris.platform.commercefacades.product.data.ProductData"> <property name="power" type="java.lang.Double"/>
</bean>

Finally, you define data transfer objects to expose software and hardware project data via the standard OCC web services.

<bean class="mypackage.SoftwareProductWsDTO" extends="de.hybris.platform.commercewebservicescommons.dto.product.ProductWsDTO"> <property name="license" type="java.lang.String"/>
</bean> <bean class="mypackage.HardwareProductWsDTO" extends="de.hybris.platform.commercewebservicescommons.dto.product.ProductWsDTO"> <property name="power" type="java.lang.Double"/>
</bean>

You define as well the corresponding field mappings and field level mappings.

<!-- Software --!>
<bean id="softwareProductFieldMapper" parent="fieldMapper"> <property name="sourceClass" value="mypackage.SoftwareProductData"/> <property name="destClass" value="mypackage.SoftwareProductWsDTO"/>
</bean> <bean parent="fieldSetLevelMapping"> <property name="dtoClass" value="mypackage.SoftwareProductWsDTO"/> <property name="levelMapping"> <map> <entry key="BASIC" value="license"/> <entry key="DEFAULT" value="BASIC"/> <entry key="FULL" value="DEFAULT"/> </map> </property>
</bean> <!-- Hardware --!>
<bean id="hardwareProductFieldMapper" parent="fieldMapper"> <property name="sourceClass" value="mypackage.HardwareProductData"/> <property name="destClass" value="mypackage.HardwareProductWsDTO"/>
</bean> <bean parent="fieldSetLevelMapping"> <property name="dtoClass" value="mypackage.HardwareProductWsDTO"/> <property name="levelMapping"> <map> <entry key="BASIC" value="power"/> <entry key="DEFAULT" value="BASIC"/> <entry key="FULL" value="DEFAULT"/> </map> </property>
</bean>

However, when you call the OCC endpoint /products/{productCode} with a software or hardware product code, it does not return the license or power fields. It only returns the fields from the ProductWsDTO object. Why? While the reason is detailed in the next section, the short answer is: SoftwareProductWsDTO and HardwareProductWsDTO are both dynamic types and require additional setup.

Why are dynamic types not working out-of-the-box?

As the documentation does not provide hints to answer this question and I really wanted an answer , I debugged the logic behind the data mapping and here is what I found. When you invoke the DataMapper.map() method, you pass the target type and the field mapping filter, also known as field level mapping.  For example:

dataMapper.map(productData, ProductWsDTO.class, "code,name,catalogVersion(catalog(id),version),supercategories(code)");

The data mapper determines first the list of allowed mappings based on the target type (e.g. ProductWsDTO) and the field mapping filter. If the target object of the mapping is called destination, the list of allowed mappings in the previous example is:

destination.code
destination.name
destination.supercategories.code
destination.catalogVersion.version
destination.catalogVersion.catalog.id

If you use field mapping levels like in the following example, the data mapper resolves the data types for the fields set with a field mapping level (e.g. supercategories is of type CategoryWsDTO) and then looks for the definition of field mapping level associated to these data types to build the list of allowed mappings.

dataMapper.map(productData, ProductWsDTO.class, "code,name,catalogVersion(DEFAULT),supercategories(DEFAULT)")

If you want to learn more about that logic, check the DefaultFieldSetBuilder class present in the webservicecommons extension.

The data mapper transfers finally data based on the configured or custom mappers but only for the allowed mappings list. For example, if productData is of type SoftwareProductData, it will use the configured mapper to transform to a SoftwareProductWsDTO but will not map the license field, because it used the field mappings defined for ProductWsDTO to determine the list of allowed mappings.

You could legitimately argue that we don’t need additional configuration for dynamic types in our scenario: by adapting the target type based on the source object, we would solve our problem. Very true but you will face later problems with other data transfer objects embedding a product, like OrderEntryWsDTO. The data mapper will recognize the type ProductWsDTO for the product attribute and we can’t tell the mapper to consider other types like SoftwareProductWsDTO. Therefore, the list of allowed mappings will never include fields defined in these other types and license as well as power will never be mapped.

How to configure dynamic types?

We need to reconfigure the fieldSetBuilder bean, responsible for generating the list of allowed mapping, so that it can take dynamic types into account. Its default implementation located in the class DefaultFieldSetBuilder already accounts for that using the subclassRegistry to lookup potential subclasses. However, the defaultFieldSetBuilder bean has not its property subclassRegistry set and consequently, by default, it never checks for subclasses. The first configuration step is to reconfigure the fieldSetBuilder to inject the subclassRegistry bean:

<alias alias="fieldSetBuilder" name="myFieldSetBuilder"/>
<bean id="myFieldSetBuilder" parent="defaultFieldSetBuilder"> <property name="subclassRegistry" ref="subclassRegistry"/>
</bean>

You should store this bean declaration in the *-web-spring.xml file of your OCC extension as the `fieldSetBuilder` bean is declared in the Spring web application context.

The second step is to register the dynamic types in the subclassRegistry, which can be done by creating a bean extending subclassMapping. The subclassRegistry bean fetches all Spring beans extending subclassMapping during its initialization.

<bean parent="subclassMapping"> <property name="parentClass" value="de.hybris.platform.commercewebservicescommons.dto.product.ProductWsDTO"/> <property name="subclassesSet"> <set> <value>com.sap.howto.pim.dto.SoftwareProductWsDTO</value> <value>com.sap.howto.pim.dto.HardwareProductWsDTO</value> </set> </property>
</bean>

You should store this bean declaration in the *-spring.xml file of your OCC extension since the subclassRegistry bean is defined in the Spring core application context.

Finally, according to the documentation, you need to create a dynamic type factory, to tell the mappers that certain source types (e.g. ProductData) are dynamic types and that the target type shall be resolved dynamically.

<bean id="customProductDataObjectFactory" class="de.hybris.platform.webservicescommons.mapping.config.DynamicTypeFactory" init-method="init"> <property name="baseType" value="de.hybris.platform.commercefacades.product.data.ProductData"/>
</bean>

You should store this bean declaration in the *-web-spring.xml file of your OCC extension. I could not really figure out why the dynamic type factory is needed, as everything was working fine without it.

That’s all what you need according to the documentation and it will work but with some limitations. As mentioned previously, the default class DefaultFieldSetBuilder does consider dynamic types when building the list of allowed mappings but only when the field filter is not based on field set levels. For example, calling /products/{productCode}?fields=code,name,license with a software product code will include the license field. However, /products/{productCode}?fields=DEFAULT will not. Why? The reason is that the default implementation resolves the field set levels only for the target types and does not include the field set levels of the their sub classes. It’s annoying but fortunately very easy to fix with the following customization:

public class MyFieldSetBuilder extends DefaultFieldSetBuilder { @Override protected Set<String> createFieldSetForLevel(final Class fieldClass, final String prefix, final String levelName, final FieldSetBuilderContext context) { final Set<String> fieldSet = super.createFieldSetForLevel(fieldClass, prefix, levelName, context); if (getSubclassRegistry() != null) { getSubclassRegistry().getSubclasses(fieldClass).forEach(fieldSubclass -> { if (!context.isRecurencyLevelExceeded(fieldSubclass)) { context.addToRecurrencyMap(fieldSubclass); fieldSet.addAll(super.createFieldSetForLevel(fieldSubclass, prefix, levelName, context)); context.removeFromRecurrencyMap(fieldSubclass); } }); } return fieldSet; }
}

You just need to adapt the fieldSetBuilder bean customization presented at the very beginning like this:

<alias alias="fieldSetBuilder" name="myFieldSetBuilder"/>
<bean id="myFieldSetBuilder" parent="defaultFieldSetBuilder" class="mypackage.MyFieldSetBuilder"> <property name="subclassRegistry" ref="subclassRegistry"/>
</bean>

That’s it! Now, everything will work as expected.

What about limitations?

You might call it limitation or feature but you will observe a new field called type in the objects based on a dynamic type, set with the name of the dynamic type (e.g. softwareProductWsDTO or hardwareProductWsDTO). It is added automatically when the web service response entity is serialized. Here is an example of response returned by OCC web service after adding a software product to the cart:

{ "entry": { "entryNumber": 0, "product": { "type": "softwareProductWsDTO", "code": "80023414", "name": "SAP Commerce 2105", "url": "/c/SAP-Commerce-2105/p/80023414", "license": "SAP" }, "quantity": 2, [...] }, [...]
}

If your object has already a field type or you want to rename the field to be more meaningful, you have to re-configure the jsonHttpMessageConverter bean as following:

<alias name="myJsonHttpMessageConverter" alias="jsonHttpMessageConverter"/>
<bean id="myJsonHttpMessageConverter" parent="defaultJsonHttpMessageConverter"> <property name="marshallerProperties"> <map merge="true"> <entry key="eclipselink.json.type-attribute-name" value="__class"/> </map> </property>
</bean>

If you want to customize the name of the dynamic type, you can do so by annotating the data transfer object bean with the @XmlType annotation.

<bean class="com.sap.howto.pim.dto.SoftwareProductWsDTO" extends="de.hybris.platform.commercewebservicescommons.dto.product.ProductWsDTO"> <import type="javax.xml.bind.annotation.XmlType"/> <annotations>@XmlType(name = "software")</annotations> [...]
</bean> <bean class="com.sap.howto.pim.dto.HardwareProductWsDTO" extends="de.hybris.platform.commercewebservicescommons.dto.product.ProductWsDTO"> <import type="javax.xml.bind.annotation.XmlType"/> <annotations>@XmlType(name = "hardware")</annotations> [...]
</bean>

The second limitation is related to the API documentation. First of all, Swagger does not know about dynamic types and does not list them under the Models section. Secondly, Swagger does not mention the dynamic types in the endpoint documentation. For example, it does not indicate that the endpoint /products/{productCode} can return one of ProductWsDTO, SoftwareProductWsDTO and HardwareProductWsDTO. The first problem can be solved by adding manually the annotation @JsonSubTypes to the base type like in the following example:

<bean class="de.hybris.platform.commercewebservicescommons.dto.product.ProductWsDTO"> <import type="com.fasterxml.jackson.annotation.JsonSubTypes"/> <import type="com.fasterxml.jackson.annotation.JsonSubTypes.Type"/> <annotations> @JsonSubTypes({ @Type(com.sap.howto.pim.dto.SoftwareProductWsDTO.class), @Type(com.sap.howto.pim.dto.HardwareProductWsDTO.class), }) </annotations>
</bean>

The second problem seems to be caused by a limitation in Swagger v2. It accepts only one response type for an endpoint and does not support subtypes. This limitation is lifted from Swagger v3 but SAP Commerce 2205 still runs Swagger v2.

Conclusion

Customizing SAP Commerce by enhancing its standard types is definitely the easiest approach. But if your scenario requires dynamic types, it is finally not a big deal. The overhead is very manageable and it could very likely payoff over time. For example, if UX/UI developers have to implement differentiated product details UI for software and hardware products, they have everything they need from the backend: they can find by themselves which data they will receive by looking at the data transfer object definitions and they can use the type field for routing.