DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Join us today at 1 PM EST: "3-Step Approach to Comprehensive Runtime Application Security"
Save your seat
  1. DZone
  2. Coding
  3. Languages
  4. Too Many Variations of Transfer Objects!

Too Many Variations of Transfer Objects!

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

Irena Shaigorodsky user avatar by
Irena Shaigorodsky
·
Feb. 12, 19 · Tutorial
Like (3)
Save
Tweet
Share
8.32K Views

Join the DZone community and get the full member experience.

Join For Free

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.

Object (computer science) Transfer (computing)

Opinions expressed by DZone contributors are their own.

Popular on DZone

  • Using QuestDB to Collect Infrastructure Metrics
  • Handling Virtual Threads
  • A Beginner's Guide to Back-End Development
  • Top Authentication Trends to Watch Out for in 2023

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com
  • +1 (919) 678-0300

Let's be friends: