Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Too Many Variations of Transfer Objects!

DZone's Guide to

Too Many Variations of Transfer Objects!

Look at an example to help delegate the decision about which variation to use.

· Integration Zone ·
Free Resource

Discover how you can get APIs and microservices to work at true enterprise scale.

The life of an API. It starts with simple calls, clean transfer objects, and minimal viable products. Over time, new client and business requirements manifest themselves as variations of the same transfer object. In most cases, there are 2 variations of an object — summary and details — that are easy to rationalize. As time passes, they are followed by the mini, midi, and everything in between versions.

The amount of boilerplate code to support the variations takes time away from solving business problems. I am going to show you an example to help delegate the decision about which variation to use to the consumers of the API. Backward compatibility will be preserved as long as existing fields are not removed.

The API does not need to be as sophisticated as the Facebook Graph API or the Google API to be flexible. With a round of applause, please welcome Jackson and Javax WS. A complete working Jersey/Guice application can be found on GitHub.

The magic starts with JsonFilter annotation:

Annotation used to indicate which logical filter is to be used for filtering out properties of type (class) annotated; association made by this annotation declaring ids of filters, and com.fasterxml.jackson.databind.ObjectMapper (or objects it delegates to) providing matching filters by id.
package my.company.service.api.model;

...
import static my.company.service.api.model.Constants.FIELD_FILTER;

@JsonFilter(FIELD_FILTER)
@Value.Immutable
@Value.Style(
...
)
@JsonDeserialize(builder = TransferObject.Builder.class)
public abstract class TransferObject {

    public abstract Optional<String> getCode();

    public abstract Optional<String> getType();

    public abstract Optional<String> getName();

...
    public static class Builder extends ImmutableTransferObject.Builder {}
}

Our Jersey application will need JacksonJsonProvider to support serialization/de-serialization of the transfer objects into JSON. In our case, the provider will use a pre-defined configuration of the ObjectMapper by default.

package my.company.service.svc.config;
...
@Provider
@Consumes(MediaType.APPLICATION_JSON) // NOTE: required to support "non-standard" JSON variants
@Produces(MediaType.APPLICATION_JSON)
public class JsonProvider extends JacksonJsonProvider {
    public JsonProvider() {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setConfig(objectMapper.getSerializationConfig())
            .setConfig(objectMapper.getDeserializationConfig())
            .setSerializationInclusion(JsonInclude.Include.NON_ABSENT)
            .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
            .configure(SerializationFeature.WRITE_DATES_WITH_ZONE_ID, true)
            .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)
            .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
            .registerModule(new JavaTimeModule())
            .registerModule(new Jdk8Module())
            .registerModule(new GuavaModule())
            .findAndRegisterModules();
        setMapper(objectMapper);
    }
}

Now let's take a look at the code below.

package my.company.service.svc.filter;

...

import static my.company.service.api.model.Constants.FIELD_FILTER;

@Singleton
/**
 * Allows to tailor the list of fields based on the list of fields specified in the request
 */
public class FieldFilteringResponseFilter implements ContainerResponseFilter {

    private static final Logger LOGGER = LoggerFactory.getLogger(FieldFilteringResponseFilter.class);

    @VisibleForTesting
    static final String FIELDS = "fields";
    private final Map<Class, Set<String>> declaredFields = 
      new ConcurrentHashMap<>();

    @Override
    public void filter(ContainerRequestContext requestContext, 
                       ContainerResponseContext responseContext)
                    throws IOException {

        Set<String> fields = getFieldsFromRequest(requestContext.getUriInfo());

        // add the modifier
        FieldObjectModifier modifier = new FieldObjectModifier(fields, 
                                                               declaredFields, 
                                                               requestContext
                                                               .getUriInfo()
                                                               .toString());
        ObjectWriterInjector.set(modifier);
    }

...
    @VisibleForTesting
    static class FieldObjectModifier extends ObjectWriterModifier {

        @VisibleForTesting
        Set<String> getFields() {
            return fields;
        }

        private final Map<Class, Set<String>> declaredFields;
        private final Set<String> fields;
        private final String uri;

        /**
         * @param fields requested fields
         * @param declaredFields all declared fields in the system
         * @param uri the uri of the request
         */
        private FieldObjectModifier(Set<String> fields, Map<Class, 
                                    Set<String>> declaredFields, String uri) {
            this.fields = fields;
            this.declaredFields = declaredFields;
            this.uri = uri;
        }

        @Override
        public ObjectWriter modify(EndpointConfigBase<?> endpoint, 
                                   MultivaluedMap<String, Object> responseHeaders, Object valueToWrite,
                        ObjectWriter objectWriter, JsonGenerator jsonGenerator) {
            SimpleBeanPropertyFilter filter = getAllowAllFilter();
            if (valueToWrite != null && fields != null && !fields.isEmpty()) {

                Set<String> availableFields = getClassInfo(valueToWrite.getClass());

                if (CollectionUtils.isNotEmpty(availableFields)) {
                    // filter out unknown fields
                    Set<String> knownFields = fields
                        .stream()
                        .filter(field -> availableFields.contains(field))
                        .collect(Collectors.toSet());

                    if (fields.size() != knownFields.size()) {
                        fields.removeAll(knownFields);
                        LOGGER.error("Unknown fields {} requested for URI {}", 
                                     fields, uri);
                    }

                    filter = new SimpleBeanPropertyFilter
                      .FilterExceptFilter(knownFields);
                }
            }
            FilterProvider provider = new SimpleFilterProvider()
              .addFilter(FIELD_FILTER, filter);
            return objectWriter.with(provider);
        }

        private SimpleBeanPropertyFilter getAllowAllFilter() {
            return SimpleBeanPropertyFilter
              .serializeAllExcept(new HashSet<String>());
        }


        private Set<String> getClassInfo(Class<?> filteredClass) {
            return declaredFields
              .computeIfAbsent(filteredClass, newKey -> computeClassInfo(newKey));
        }

        private Set<String> computeClassInfo(Class filteredClass) {
            BeanInfo info;
            try {
                info = Introspector.getBeanInfo(filteredClass);
            } catch (IntrospectionException e) {
                return Collections.emptySet();
            }
            PropertyDescriptor[] props = info.getPropertyDescriptors();

            return ImmutableSet.copyOf(
                Arrays.stream(props)
                    .map(prop -> prop.getName()).collect(Collectors.toSet()));
        }
    }
}

As you can see above on line 33, the ObjectWriterInjector sets an instance of ObjectWriterModifier, based on the request parameter. This allows for the customization of the ObjectWriter to keep only the fields the client requested. Behind the scenes, ObjectWriterInjector is used in the writeTo method of the ProviderBase, which is the parent class of JacksonJsonProvider.

Another benefit of this approach is that the expensive information is loaded only when requested.

package my.company.service.svc;

import static my.company.service.svc.filter.FieldFilteringResponseFilter
.getFieldsFromRequest;
...
@Path(MyService.ROOT_PATH)
public class MyServiceImpl implements MyService {
...
    @Context
    UriInfo uriInfo;

    @Override
    public TransferObject getTransferObject() {
        Set<String> fields = getFieldsFromRequest(uriInfo);
        if (CollectionUtils.isEmpty(fields) || fields.contains("features")) {
            // TODO do heavy lifting to load the features
        }
        return TRANSFER_OBJECT;
    }
}

The application details can be found on GitHub. Let me know your thoughts in the comments.

APIs and microservices are maturing, quickly. Learn what it takes to manage modern APIs and microservices at enterprise scale.

Topics:
jackson ,integration ,tutorial ,json ,api ,variations ,transfer objects

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}