Skip to content

SDA Commons Server Jackson

javadoc

The module sda-commons-server-jackson is used to configure the ObjectMapper with the recommended default settings of SDA SE services. It also provides support for linking resources with HAL and adds the ability to filter fields on client request.

The JacksonConfigurationBundle is used to configure the JSON serializer. It adds various error mappers to support the SDA error message standard. These replace the default Dropwizard error mappers but also additional new mappers are added, e.g. mapping JaxRs Exceptions, such as NotFound and NotAuthorized. All mappers do log the errors when mapping. The framework already provides a predefined exception mapper for InvalidTypeException if the framework default for feature FAIL_ON_INVALID_SUBTYPE is customized. See section Customize the ObjectMapper below for details about customizing the default object mapper settings. If the feature is enabled the InvalidTypeIdExceptionMapper produces a response in the common error format.

The ObjectMapperConfigurationUtil can be used to receive an ObjectMapper with the recommended settings for usage outside a Dropwizard application.

The default ObjectMapper is configured to be fault-tolerant to avoid failures in deserialization. JSR-303 validations should be used to validate input data. For serialization the bundle disables FAIL_ON_EMPTY_BEANS, WRITE_DATES_AS_TIMESTAMPS, WRITE_DURATIONS_AS_TIMESTAMPS. For deserialization the bundle disables FAIL_ON_UNKNOWN_PROPERTIES, FAIL_ON_IGNORED_PROPERTIES, FAIL_ON_INVALID_SUBTYPE and enables ACCEPT_SINGLE_VALUE_AS_ARRAY, READ_UNKNOWN_ENUM_VALUES_AS_NULL, READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE. The FuzzyEnumModule from Dropwizard is removed as it lacks support of newer Jackson features for enumerations.

Usage

In the application class, the bundle is added in the initialize method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import JacksonConfigurationBundle;
import io.dropwizard.core.Application;

public class MyApplication extends Application<MyConfiguration> {

    public static void main(final String[] args) {
        new MyApplication().run(args);
    }

   @Override
   public void initialize(Bootstrap<MyConfiguration> bootstrap) {
      // ...
      bootstrap.addBundle(JacksonConfigurationBundle.builder().build());
      // ...
   }

   @Override
   public void run(MyConfiguration configuration, Environment environment) {
      // ...
   }
}

Date and Time formatting

It is strongly recommended to use

  • LocalDate for dates without time
  • ZonedDateTime for date and times
  • Duration for durations with time resolution
  • Period for durations with day resolution

All these types can be read and written in JSON as ISO 8601 formats. ZonedDateTime is formatted with milliseconds or nanoseconds according to the detail set in the instance.

Type Resolution Example
LocalDate Day of Month 2018-09-23
ZonedDateTime Any time unit 2018-09-23T14:21:41.123+01:00
Duration Any time unit P1DT13M
Period Days P1Y2D

Reading ZonedDateTime is configured to be tolerant so that added nanoseconds or missing milliseconds or missing seconds are supported.

Please do not use @JsonFormat(pattern = "...") for customizing serialization because it breaks tolerant reading of formatting variants. If output should be customized, use @JsonSerializer.

SDA-Commons provides two default serializers for ZonedDateTime to use a fixed resolution in the output. Iso8601Serializer is used to omit milliseconds and Iso8601Serializer.WithMillis is used to render the time with 3 digits for milliseconds.

Usage:

1
2
3
4
5
6
7
8
9
class MyResource {
   @JsonSerialize(using = org.sdase.commons.server.jackson.Iso8601Serializer.class)
   private ZonedDateTime zonedDateTime;

   @JsonSerialize(using = org.sdase.commons.server.jackson.Iso8601Serializer.WithMillis.class)
   private ZonedDateTime zonedDateTimeWithMillis;

   // ...
}

Resources that should be processed for HAL links must be annotated as @Resource. Links are added directly in the resource class and are annotated as @Link. Embedded resources can be added as @EmbeddedResource. The Open API Tools are used to render them in appropriate _links and _embedded properties. Links are properly documented in Swagger when io.openapitools.hal:swagger-hal is added to the dependencies. io.openapitools.hal:swagger-hal is shipped with sda-commons-server-openapi.

HAL link support may be disabled in the JacksonConfigurationBundle.builder().

Example:

1
2
3
4
5
6
7
@Resource
public class Person {
   @Link 
   private HALLink self;
   private String name;
   // ...
}
1
2
3
GET /persons/123

=> {"_links":{"self":{"href":"/persons/123"}},"name":"John Doe"}

EmbedHelper

To decide whether a resource is just linked or embedded, the EmbedHelper can be used. If query parameters for embedding are passed, like /api/cars?embed=drivers,owner, EmbedHelper.isEmbeddingOfRelationRequested(relationName) can be used to check whether a resource should be embedded:

1
2
3
4
5
6
7
EmbedHelper embedHelper = new EmbedHelper(environment);

...

if (embedHelper.isEmbeddingOfRelationRequested("owner")) {
   carResource.setOwner(createPerson(ownerId));
}

In an application that uses CDI the EmbedHelper should be instantiated the same way and provided by a producer method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
   private EmbedHelper embedHelper;

   @Override
   public void run(Configuration config, Environment environment) {
      // ...
      this.embedHelper = new EmbedHelper(environment);
   }

   @Produces
   public EmbedHelper embedHelper() {
      return this.embedHelper;
   }

Field filtering feature for resources

The JacksonConfigurationBundle registers the JacksonFieldFilterModule, which adds the FieldFilterSerializerModifier. The modifier reads the fields query parameter from the JAX-RS request context. If fields is present, only requested fields are rendered. Field names can be comma-separated or provided through repeated query parameters. Whitespace is ignored.

Field filtering is enabled per serialized type with @EnableFieldFilter. If no fields parameter is provided, the complete response is rendered. HAL links and embedded resources are rendered regardless of the requested fields.

Field filtering support may be disabled in the JacksonConfigurationBundle.builder().

Response fixtures below omit HAL _links; field-filtering tests still verify links remain present.

Example model:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.util.List;
import java.util.Map;
import org.sdase.commons.server.jackson.EnableFieldFilter;

@EnableFieldFilter(enableNestedPathFiltering = true)
record Person(
    String id,
    String firstName,
    String lastName,
    String nickName,
    List<Child> children,
    Address address,
    Map<String, Attribute> attributes,
    NestedResource renamedCustomProp,
    UnfilteredChild unfilteredChild,
    List<UnfilteredChild> unfilteredChildren) {}

@EnableFieldFilter(enableNestedPathFiltering = true)
record Child(String nickName, String firstName, String lastName) {}

@EnableFieldFilter(enableNestedPathFiltering = true)
record Address(String id, String city) {}

@EnableFieldFilter(enableNestedPathFiltering = true)
record Attribute(String name, String description) {}

@EnableFieldFilter(enableNestedPathFiltering = true)
record NestedResource(String myNestedField, int someNumber, NestedNestedResource myNestedResource) {}

@EnableFieldFilter(enableNestedPathFiltering = true)
record NestedNestedResource(String anotherNestedField, int someNumber) {}

record UnfilteredChild(String name, String lastName) {}

To enable field filtering for a resource, annotate the serialized type with @EnableFieldFilter.

1
GET /people/jdoe?fields=firstName,nickName
1
2
3
4
{
  "firstName": "John",
  "nickName": "Johnny"
}

Nested fields

Nested paths use dot notation. Nested filtering is configured per annotated type. A nested type must enable enableNestedPathFiltering to filter its own properties; parent and child settings are independent:

1
GET /people/jdoe-and-children-with-flag?fields=children.nickName,renamedCustomProp.myNestedResource.anotherNestedField,address.city
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{
  "children": [
    {
      "nickName": "Yassie"
    }
  ],
  "address": {
    "city": "Hamburg"
  },
  "renamedCustomProp": {
    "myNestedResource": {
      "anotherNestedField": "deep"
    }
  }
}

By default, enableNestedPathFiltering is false. Once a parent property is selected, its complete subtree is kept. For example:

1
GET /people/jdoe-and-children?fields=children,address
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  "children": [
    {
      "firstName": "Yasmine",
      "lastName": "Doe",
      "nickName": "Yassie"
    }
  ],
  "address": {
    "id": "Hamburg",
    "city": "Hamburg"
  }
}

If a nested object is not annotated with @EnableFieldFilter, selecting one of its sub-fields keeps the complete nested object. The enableNestedPathFiltering setting is evaluated per annotated type.

1
GET /people/jdoe-and-children?fields=unfilteredChild.name
1
2
3
4
5
6
{
  "unfilteredChild": {
    "name": "Jane",
    "lastName": "Doey"
  }
}

The same rule applies to unannotated list items:

1
GET /people/jdoe-and-children-with-flag?fields=unfilteredChildren.name
1
2
3
4
5
6
7
8
{
  "unfilteredChildren": [
    {
      "name": "Janet",
      "lastName": "Doe"
    }
  ]
}
Nested filtering on maps

Requesting attributes.name filters each map value. Map keys are not path segments. To filter fields inside map values, nested filtering must be enabled on the serialized container type and on the map value type with @EnableFieldFilter(enableNestedPathFiltering = true). If either type does not enable nested path filtering, the complete map value is kept.

1
GET /people/attributes-with-flag?fields=attributes.name
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  "attributes": {
    "alpha": {
      "name": "first"
    },
    "beta": {
      "name": "second"
    }
  }
}

Requesting parent map keeps complete values:

1
GET /people/attributes-with-flag?fields=attributes
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
  "attributes": {
    "alpha": {
      "name": "first",
      "description": "hidden-one"
    },
    "beta": {
      "name": "second",
      "description": "hidden-two"
    }
  }
}
Nested filtering on lists

The same rules apply to list items. Requesting a nested path filters each item in the list. To filter fields inside list items, nested filtering must be enabled on the serialized parent type and on the list item type with @EnableFieldFilter(enableNestedPathFiltering = true). If either type does not enable nested path filtering, the complete list item is kept.

1
GET /people/jdoe-and-children-with-flag?fields=children.nickName
1
2
3
4
5
6
7
{
  "children": [
    {
      "nickName": "Yassie"
    }
  ]
}

Requesting the parent list keeps the complete subtree of each item.

1
GET /people/jdoe-and-children?fields=children
1
2
3
4
5
6
7
8
9
{
  "children": [
    {
      "firstName": "Yasmine",
      "lastName": "Doe",
      "nickName": "Yassie"
    }
  ]
}

HAL links remain included; omitted from fixtures for stable documentation output.

Wrapped lists

For wrapped lists, include wrapper property in field path. Wrapper and item types must be annotated when nested item filtering is enabled.

1
GET /people/search-result-list?fields=results.name
1
2
3
4
5
6
7
8
{
  "results": [
    {
      "name": "bundle-a",
      "customMap": null
    }
  ]
}

Configuration

Disable HAL support

The JacksonConfigurationBundle may be initialized without HAL support, if links are not needed or achieved in another way in the application:

1
JacksonConfigurationBundle.builder().withoutHalSupport().build();

Customize the ObjectMapper

Custom configurations of the ObjectMapper can be achieved by adding a customization consumer which receives the used ObjectMapper instance:

1
2
3
JacksonConfigurationBundle.builder()
    .withCustomization(om -> om.enable(SerializationFeature.INDENT_OUTPUT))
    .build();

YAML

If the JacksonYAMLProvider is available in the classpath, it will be registered to support requests that Accept application/yaml. This is especially useful for Swagger which provides the swagger.json also as swagger.yaml.

To activate YAML support, a dependency to com.fasterxml.jackson.jaxrs:jackson-jaxrs-yaml-provider has to be added. It is shipped in an appropriate version with sda-commons-server-openapi.

Error Format

Exceptions are mapped to a common error format that looks like the following example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
422 Unprocessable Entity
{
    "title": "Request parameters are not valid",
    "invalidParams": [
         {
            "field": "manufacture",
            "reason": "Audi has no Golf GTI model (not found)"
            "errorCode": "FIELD_CORRELATION_ERROR"
        },
        {
            "field": "model",
            "reason": "Golf GTI is unkown by Audi (not found)"
            "errorCode": "FIELD_CORRELATION_ERROR"
        }
    ]
}

For validation errors, the invalidParams section is filled. For other errors, just a title is given.

  • "field" defines the invalid field within the JSON structure
  • "reason" gives a hint why the value is not valid. This is the error message of the validation.
  • "errorCode" is the validation annotation given in uppercase underscore notation

The reason might be in different language due to internationalization.

Examples how exceptions and the error structure should be used, can be found within the example project sda-commons-server-errorhandling-example