1. Introduction
Web content is shared using many standardized MIME Types. We will be addressing two of them here
-
XML
-
JSON
I will show manual approaches to marshaling/unmarshalling first. However, content is automatically marshalled/unmarshalled by the web client container once everything is set up properly. Manual marshaling/unmarshalling approaches are mainly useful in determining provider settings and annotations — as well as to perform low-level development debug outside the server on the shape and content of the payloads.
1.1. Goals
The student will learn to:
-
identify common/standard information exchange content types for web API communications
-
manually marshal and unmarshal Java types to and from a data stream of bytes for multiple content types
-
negotiate content type when communicating using web API
-
pass complex Data Transfer Objects to/from a web API using different content types
-
resolve data mapping issues
1.2. Objectives
At the conclusion of this lecture and related exercises, the student will be able to:
-
design a set of Data Transfer Objects (DTOs) to render information from and to the service
-
define Java class content type mappings to customize marshalling/unmarshalling
-
specify content types consumed and produced by a controller
-
specify content types supplied and accepted by a client
2. Pattern Data Transfer Object
There can be multiple views of the same conceptual data managed by a service. They can be the same physical implementation — but they serve different purposes that must be addressed. We will be focusing on the external client view (Data Transfer Object (DTO)) during this and other web tier lectures. I will specifically contrast the DTO with the internal implementation view (Business Object (BO)) right now to help us see the difference in the two roles.
2.1. DTO Pattern Problem Space
|
Figure 1. Clients and Service Sharing Implementation Data
|
- Problem
-
Issues can arise when service implementations are complex.
-
client may get data they do not need
-
client may get data they cannot handle
-
client may get data they are not authorized to use
-
client may get too much data to be useful (e.g., entire database serialized to client)
-
- Forces
-
The following issues are assumed to be true:
-
some clients are local and can share object references with business logic
-
handling specifics of remote clients is outside core scope of business logic
-
2.2. DTO Pattern Solution Space
|
Figure 2. DTO Represents Client View of Data
|
DTO/BO Mapping Location is a Design Choice
The design decision of which layer translates between DTOs of the API and BOs of the service is not always fixed. Since the DTO is an interface pattern and the Web API is one of many possible interface facades and clients of the service — the job of DTO/BO mapping may be done in the service tier instead. |
2.3. DTO Pattern Players
- Data Transfer Object
-
-
represents a subset of the state of the application at a point in time
-
not dependent on Business Objects or server-side technologies
-
doing so would require sending Business Objects to client
-
-
XML and JSON provide the “ultimate isolation” in DTO implementation/isolation
-
- Remote (Web) Facade
-
-
uses Business Logic and DTOs to perform core business logic
-
manages interface details with client
-
- Business Logic
-
-
performs core implementation duties that may include interaction with backend services and databases
-
- Business Object (Entity)
-
-
representation of data required to implement service
-
may have more server-side-specific logic when DTOs are present in the design
-
DTOs and BOs can be same class(es) in simple or short-lived services
DTOs and BOs can be the same class in small services.
However, supporting multiple versions of clients over longer service lifetimes may cause even small services to split the two data models into separate implementations.
|
3. Sample DTO Class
The following is an example DTO class we will look to use to
represent client view of data in a simple "Quote Service". The QuoteDTO
class
can start off as a simple POJO and — depending on the binding (e.g., JSON
or XML) and binding library (e.g., Jackson, JSON-B, or JAXB) - we may have
to add external configuration and annotations to properly shape
our information exchange.
The class is a vanilla POJO with a default constructor, public getters and setters,
and other convenience methods — mostly implemented by Lombok. The quote contains
three different types of fields (int, String, and LocalDate). The date
field
is represented using java.time.LocalDate
.
package info.ejava.examples.svc.content.quotes.dto;
import lombok.*;
import java.time.LocalDate;
@NoArgsConstructor (1)
@AllArgsConstructor
@Data (2)
@Builder
@With
public class QuoteDTO {
private int id;
private String author;
private String text;
private LocalDate date; (3)
private String ignored; (4)
}
1 | default constructor |
2 | public setters and getters |
3 | using Java 8, java.time.LocalDate to represent generic day of year without timezone |
4 | example attribute we will configure to be ignored |
Lombok @Builder and @With
@Builder will create a new instance of the class using incrementally defined properties.
@With creates a copy of the object with a new value for one of its properties.
@Builder can be configured to create a copy constructor (i.e., a copy builder with no property value change).
|
Lombok @Builder and Constructors
@Builder requires an all-args-ctor and will define a package-friendly one unless there is already a ctor defined.
Unmarshallers require a no-args-ctor and can be provided using @NoArgsConstructor .
The presence of the no-args-ctor turns off the required all-args-ctor for @Builder and can be re-enabled with @AllArgsConstructor .
|
4. Time/Date Detour
While we are on the topic of exchanging data — we might as well address time-related data that can cause numerous mapping issues. Our issues are on multiple fronts.
-
what does our time-related property represent?
-
e.g., a point in time, a point in time in a specific timezone, a birthdate, a daily wake-up time
-
-
what type do we use to represent our expression of time?
-
do we use legacy Date-based types that have a lot of support despite ambiguity issues?
-
do we use the newer
java.time
types that are more explicit in meaning but have not fully caught on everywhere?
-
-
how should we express time within the marshalled DTO?
-
how can we properly unmarshal the time expression into what we need?
-
how can we handle the alternative time wire expressions with minimal pain?
4.1. Pre Java 8 Time
During pre-Java8, we primarily had the following time-related java.util
classes
represents a point in time without timezone or calendar information. The point is a Java long value that represents the number of milliseconds before or after 1970 UTC. This allows us to identify a millisecond between 292,269,055 BC and 292,278,994 AD when applied to the Gregorian calendar. |
|
interprets a Date according to an assigned calendar (e.g., Gregorian Calendar) into years, months, hours, etc. Calendar can be associated with a specific timezone offset from UTC and assumes the Date is relative to that value. |
During the pre-Java 8 time period, there was also a time-based library called Joda that became popular at providing time expressions that more precisely identified what was being conveyed.
4.2. java.time
The ambiguity issues with java.util.Date
and the expression and popularity of
Joda caused it to be adopted into Java 8 (
JSR 310).
The following are a few of the key java.time
constructs added in Java 8.
represents a point in time at 00:00 offset from UTC. The point is a nanosecond
and improves on |
|
adds |
|
adds timezone identity to OffsetDateTime — which can be used to determine the appropriate timezone offset (i.e., daylight savings time) ( |
|
a generic date, independent of timezone and time ( |
|
a generic time of day, independent of timezone or specific date.
This allows us to express "I set my alarm for 6am" - without specifying the actual dates that is performed ( |
|
a date and time but lacking a specific timezone offset from UTC ( |
|
a time-based amount of time (e.g., 30.5 seconds). Vocabulary supports from milliseconds to days. |
|
a date based amount of time (e.g., 2 years and 1 day). Vocabulary supports from days to years. |
4.3. Date/Time Formatting
There are two primary format frameworks for formatting and parsing time-related fields in text fields like XML or JSON:
This legacy |
|
This newer |
public static final DateTimeFormatter ISO_LOCAL_DATE_TIME;
static {
ISO_LOCAL_DATE_TIME = new DateTimeFormatterBuilder()
.parseCaseInsensitive()
.append(ISO_LOCAL_DATE)
.appendLiteral('T')
.append(ISO_LOCAL_TIME)
.toFormatter(ResolverStyle.STRICT, IsoChronology.INSTANCE);
}
This, wrapped with some optional and default value constructs to handle missing information makes for a pretty powerful time parsing and formatting tool.
4.4. Date/Time Exchange
There are a few time standards supported by Java date/time formatting frameworks:
ISO 8601 |
This standard is cited in many places but hard to track down an official example of each and every format — especially when it comes to 0 values and timezone offsets.
However, an example representing a ZonedDateTime and EST may look like the following: |
RFC 822/ RFC 1123 |
These are lesser followed standards for APIs and includes constructs like an English word
abbreviation for day of week and month. The DateTimeFormatter example for this group
is |
My examples will work exclusively with the ISO 8601 formats. The following example leverages the Java expression of time formatting to allow for multiple offset expressions (Z
, +00
, +0000
, and +00:00
) on top of a standard LOCAL_DATE_TIME expression.
public static final DateTimeFormatter UNMARSHALLER
= new DateTimeFormatterBuilder()
.parseCaseInsensitive()
.append(DateTimeFormatter.ISO_LOCAL_DATE)
.appendLiteral('T')
.append(DateTimeFormatter.ISO_LOCAL_TIME)
.parseLenient()
.optionalStart().appendOffset("+HH", "Z").optionalEnd()
.optionalStart().appendOffset("+HH:mm", "Z").optionalEnd()
.optionalStart().appendOffset("+HHmm", "Z").optionalEnd()
.optionalStart().appendLiteral('[').parseCaseSensitive()
.appendZoneRegionId()
.appendLiteral(']').optionalEnd()
.parseDefaulting(ChronoField.OFFSET_SECONDS,0)
.parseStrict()
.toFormatter();
Use ISO_LOCAL_DATE_TIME Formatter by Default
Going through the details of |
5. Java Marshallers
I will be using four different data marshalling providers during this lecture:
|
the default JSON provider included within Spring and Spring Boot. It implements its own proprietary interface for mapping Java POJOs to JSON text. |
|
a relatively new Jakarta EE standard for JSON marshalling. The reference implementation is Yasson from the open source Glassfish project. It will be used to verify and demonstrate portability between the built-in Jackson JSON and other providers. |
|
a tightly integrated sibling of Jackson JSON. This requires a few extra module dependencies but offers a very similar setup and annotation set as the JSON alternative. I will use Jackson XML as my primary XML provider during examples. |
|
a well-seasoned XML marshalling framework that was the foundational requirement for early JavaEE servlet containers. I will use JAXB to verify and demonstrate portability between Jackson XML and other providers. |
Spring Boot comes with a Jackson JSON pre-wired with the web dependencies. It seamlessly
gets called from RestTemplate, RestClient, WebClient and the RestController when application/json
or nothing has been selected. Jackson XML requires additional dependencies — but integrates just
as seamlessly with the client and server-side frameworks for application/xml
.
For those reasons — Jackson JSON and Jackson XML will be used as our core marshalling
frameworks. JSON-B and JAXB will just be used for portability testing.
6. JSON Content
JSON is the content type most preferred by Javascript UI frameworks and NoSQL databases. It has quickly overtaken XML as a preferred data exchange format.
{
"id" : 0,
"author" : "Hotblack Desiato",
"text" : "Parts of the inside of her head screamed at other parts of the inside of her head.",
"date" : "1981-05-15"
}
Much of the mapping can be accomplished using Java reflection. Provider-specific
annotations can be added to address individual issues. Let’s take a look at how
both Jackson JSON and JSON-B can be used to map our QuoteDTO
POJO to the above
JSON content. The following is a trimmed down copy of the DTO class I showed you earlier.
What kind of things do we need to make that mapping?
@Data
public class QuoteDTO {
private int id;
private String author;
private String text;
private LocalDate date; (1)
private String ignored; (2)
}
1 | may need some LocalDate formatting |
2 | may need to mark as excluded |
6.1. Jackson JSON
For the simple cases, our DTO classes can be mapped to JSON with minimal effort using Jackson JSON. However, we potentially need to shape our document and can use Jackson annotations to customize. The following example shows using an annotation to eliminate a property from the JSON document.
Example Pre-Tweaked JSON Payload
|
Example QuoteDTO with Jackson Annotation(s)
|
Date/Time Formatting Handled at ObjectMapper/Marshaller Level
The example annotation above only addressed the ignore property.
We will address date/time formatting at the ObjectMapper/marshaller level below.
|
6.1.1. Jackson JSON Initialization
Jackson JSON uses an ObjectMapper
class to go to/from POJO and JSON. We can configure
the mapper with options or configure a reusable builder to create mappers with
prototype options.
Choosing the latter approach will be useful once we move inside the server.
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
We have the ability to simply create a default ObjectMapper directly.
ObjectMapper mapper = new ObjectMapper();
However, when using Spring it is useful to use the Spring Jackson2ObjectMapperBuilder
class to set many of the data marshalling types for us.
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
...
ObjectMapper mapper = new Jackson2ObjectMapperBuilder()
.featuresToEnable(SerializationFeature.INDENT_OUTPUT) (1)
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) (2)
//more later
.createXmlMapper(false) (3)
.build();
1 | optional pretty print indentation |
2 | option to use ISO-based strings versus binary values and arrays |
3 | same Spring builder creates both XML and JSON ObjectMappers |
Use Injection When Inside Container
When inside the container, have the Jackson2ObjectMapperBuilder injected (i.e., not locally-instantiated) in order to pick up external and property configurations/customizations.
|
By default, Jackson will marshal zone-based timestamps as a decimal number
(e.g., -6106031876.123456789
) and generic date/times as an array of values
(e.g., [ 1776, 7, 4, 8, 2, 4, 123456789 ]
and [ 1966, 1, 9 ]
). By disabling this serialization
feature, Jackson produces ISO-based strings for all types of timestamps and
generic date/times (e.g., 1776-07-04T08:02:04.123456789Z
and
2002-02-14
)
The following example from the class repository shows a builder customizer being registered as a @Bean
factory to be able to adjust Jackson defaults used by the server.
The returned lambda function is called with a builder each time someone injects a Jackson2ObjectMapper
— provided the Jackson AutoConfiguration has not been overridden.
/**
* Execute these customizations first (Highest Precedence) and then the
* properties second so that properties can override Java configuration.
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public Jackson2ObjectMapperBuilderCustomizer jacksonMapper() {
return (builder) -> { builder
//spring.jackson.serialization.indent-output=true
.featuresToEnable(SerializationFeature.INDENT_OUTPUT)
//spring.jackson.serialization.write-dates-as-timestamps=false
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
//spring.jackson.date-format=info.ejava.examples.svc.content.quotes.dto.ISODateFormat
.dateFormat(new ISODateFormat());
};
}
6.1.2. Jackson JSON Marshalling/Unmarshalling
The mapper created from the builder can then be used to marshal the POJO to JSON.
private ObjectMapper mapper;
public <T> String marshal(T object) throws IOException {
StringWriter buffer = new StringWriter();
mapper.writeValue(buffer, object);
return buffer.toString();
}
The mapper can just as easy — unmarshal the JSON to a POJO instance.
public <T> T unmarshal(Class<T> type, String buffer) throws IOException {
T result = mapper.readValue(buffer, type);
return result;
}
A packaged set of marshal/unmarshal convenience routines have been packaged inside ejava-dto-util
.
6.1.3. Jackson JSON Maven Aspects
For modules with only DTOs with Jackson annotations, only a direct dependency on jackson-annotations is necessary.
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
Modules that will be marshalling/unmarshalling JSON will need the core libraries that can be conveniently brought in through a dependency on one of the following two starters.
-
spring-boot-starter-web
-
spring-boot-starter-json
org.springframework.boot:spring-boot-starter-web:jar
+- org.springframework.boot:spring-boot-starter-json:jar
| +- com.fasterxml.jackson.core:jackson-databind:jar
| | +- com.fasterxml.jackson.core:jackson-annotations:jar
| | \- com.fasterxml.jackson.core:jackson-core
| +- com.fasterxml.jackson.datatype:jackson-datatype-jdk8:jar (1)
| +- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:jar (1)
| \- com.fasterxml.jackson.module:jackson-module-parameter-names:jar
1 | defines mapping for java.time types |
Jackson has built-in ISO mappings for Date and java.time
Jackson has built-in mappings to ISO for java.util.Date and java.time
data types.
|
6.2. JSON-B
JSON-B (the standard) and
Yasson (the reference implementation of JSON-B) can pretty much
render a JSON view of our simple DTO class right
out of the box. Customizations can be applied using
JSON-B annotations. In the following example, the ignore
Java property
is being excluded from the JSON output.
Example Pre-Tweaked JSON-B Payload
|
Example QuoteDTO with JSON-B Annotation(s)
|
6.2.1. JSON-B Initialization
JSON-B provides all mapping through a Jsonb
builder object
that can be configured up-front with various options.
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
import jakarta.json.bind.JsonbConfig;
JsonbConfig config=new JsonbConfig()
.setProperty(JsonbConfig.FORMATTING, true); (1)
Jsonb builder = JsonbBuilder.create(config);
1 | adds pretty-printing features to payload |
Jsonb is no longer
javax The Jsonb package has changed from
|
6.2.2. JSON-B Marshalling/Unmarshalling
The following two examples show how JSON-B marshals and unmarshals the DTO POJO instances to/from JSON.
private Jsonb builder;
public <T> String marshal(T object) {
String buffer = builder.toJson(object);
return buffer;
}
public <T> T unmarshal(Class<T> type, String buffer) {
T result = (T) builder.fromJson(buffer, type);
return result;
}
6.2.3. JSON-B Maven Aspects
Modules defining only the DTO class require a dependency on the following API definition for the annotations.
<dependency>
<groupId>jakarta.json</groupId>
<artifactId>jakarta.json-api</artifactId>
</dependency>
Modules marshalling/unmarshalling JSON documents using JSON-B/Yasson implementation
require dependencies on binding-api
and a runtime dependency on yasson
implementation.
org.eclipse:yasson:jar
+- jakarta.json.bind:jakarta.json.bind-api:jar
+- jakarta.json:jakarta.json-api:jar
\- org.glassfish:jakarta.json:jar
7. XML Content
XML is preferred by many data exchange services that require rigor in their data definitions.
That does not mean that rigor is always required. The following two examples are
XML renderings of a QuoteDTO
.
The first example is a straight mapping of Java class/attribute to XML elements. The second example applies an XML namespace and attribute (for the id
property).
Namespaces become important when mixing similar data types from different sources. XML attributes are commonly used to host identity information. XML elements are commonly used for description information.
The sometimes arbitrary use of attributes over elements in XML leads to some confusion when trying to perform direct mappings between JSON and XML — since JSON has no concept of an attribute.
<QuoteDTO> (1)
<id>0</id> (2)
<author>Zaphod Beeblebrox</author>
<text>Nothing travels faster than the speed of light with the possible exception of bad news, which obeys its own special laws.</text>
<date>1927</date> (3)
<date>6</date>
<date>11</date>
<ignored>ignored</ignored> (4)
</QuoteDTO>
1 | root element name defaults to variant of class name |
2 | all properties default to @XmlElement mapping |
3 | java.time types are going to need some work |
4 | all properties are assumed to not be ignored |
Collections Marshall Unwrapped
The three (3) date elements above are elements of an ordered collection marshalled without a wrapping element.
If we wanted to keep the collection (versus marshalling in ISO format), it would be common to define a wrapping element to encapsulate the collection — much like parentheses in a sentence.
|
<quote xmlns="urn:ejava.svc-controllers.quotes" id="0"> (1) (2) (3)
<author>Zaphod Beeblebrox</author>
<text>Nothing travels faster than the speed of light with the possible exception of bad news, which obeys its own special laws.</text>
<date>1927-06-11</date>
</quote> (4)
1 | quote is our targeted root element name |
2 | urn:ejava.svc-controllers.quotes is our targeted namespace |
3 | we want the id mapped as an attribute — not an element |
4 | we want certain properties from the DTO not to show up in the XML |
7.1. Jackson XML
Like Jackson JSON, Jackson XML will attempt to map a Java class solely on Java reflection and default mappings. However, to leverage key XML features like namespaces and attributes, we need to add a few annotations. The partial example below shows our POJO with Lombok and other mappings excluded for simplicity.
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
...
@JacksonXmlRootElement(localName = "quote", (1)
namespace = "urn:ejava.svc-controllers.quotes") (2)
public class QuoteDTO {
@JacksonXmlProperty(isAttribute = true) (3)
private int id;
private String author;
private String text;
private LocalDate date;
@JsonIgnore (4)
private String ignored;
}
1 | defines the element name when rendered as the root element |
2 | defines namespace for type |
3 | maps id property to an XML attribute — default is XML element |
4 | reuses Jackson JSON general purpose annotations |
7.1.1. Jackson XML Initialization
Jackson XML initialization is nearly identical to its JSON sibling as long as we want them to have the same options. In all of our examples I will be turning off array-based, numeric dates expression in favor of ISO-based strings.
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
XmlMapper mapper = new Jackson2ObjectMapperBuilder()
.featuresToEnable(SerializationFeature.INDENT_OUTPUT) (1)
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) (2)
//more later
.createXmlMapper(true) (3)
.build();
1 | pretty print output |
2 | use ISO-based strings for time-based fields versus binary numbers and arrays |
3 | XmlMapper extends ObjectMapper |
7.1.2. Jackson XML Marshalling/Unmarshalling
public <T> String marshal(T object) throws IOException {
StringWriter buffer = new StringWriter();
mapper.writeValue(buffer, object);
return buffer.toString();
}
public <T> T unmarshal(Class<T> type, String buffer) throws IOException {
T result = mapper.readValue(buffer, type);
return result;
}
7.1.3. Jackson XML Maven Aspects
Jackson XML is not broken out into separate libraries as much as its JSON sibling. Jackson XML annotations are housed in the same library as the marshalling/unmarshalling code.
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
7.2. JAXB
JAXB is more particular about the definition of the Java class to be mapped.
JAXB requires that the root element of a document be defined with
an @XmlRootElement
annotation with an optional name and namespace
defined.
com.sun.istack.SAXException2: unable to marshal type
"info.ejava.examples.svc.content.quotes.dto.QuoteDTO"
as an element because it is missing an @XmlRootElement annotation
...
import jakarta.xml.bind.annotation.XmlRootElement;
...
@XmlRootElement(name = "quote", namespace = "urn:ejava.svc-controllers.quotes")
public class QuoteDTO { (1) (2)
1 | default name is quoteDTO if not supplied |
2 | default to no namespace if not supplied |
JAXB 4.x is no longer
javax JAXB 4.x used in Spring Boot 3/Spring 6 is no longer
|
7.2.1. Custom Type Adapters
JAXB has no default definitions for java.time
classes and must be handled with custom adapter code.
INFO: No default constructor found on class java.time.LocalDate java.lang.NoSuchMethodException: java.time.LocalDate.<init>()
This has always been an issue for Date formatting even before java.time
and can easily be solved with a custom adapter class that converts between a String and the unsupported
type.
We can locate
packaged solutions on the web, but it is helpful to get comfortable with the process on our own.
We first create an adapter class that extends XmlAdapter<ValueType, BoundType>
— where ValueType is a type known to JAXB and BoundType is the type we are mapping.
We can use DateFormatter.ISO_LOCAL_DATE to marshal and unmarshal the LocalDate to/from text.
import jakarta.xml.bind.annotation.adapters.XmlAdapter;
...
public static class LocalDateJaxbAdapter extends extends XmlAdapter<String, LocalDate> {
@Override
public LocalDate unmarshal(String text) {
return text == null ? null : LocalDate.parse(text, DateTimeFormatter.ISO_LOCAL_DATE);
}
@Override
public String marshal(LocalDate timestamp) {
return timestamp==null ? null : DateTimeFormatter.ISO_LOCAL_DATE.format(timestamp);
}
}
We next annotate the Java property with @XmlJavaTypeAdapter
, naming our
adapter class.
import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
...
@XmlAccessorType(XmlAccessType.FIELD) (2)
public class QuoteDTO {
...
@XmlJavaTypeAdapter(LocalDateJaxbAdapter.class) (1)
private LocalDate date;
1 | custom adapter required for unsupported types |
2 | must manually set access to FIELD when annotating attributes |
7.2.2. JAXB Initialization
There is no sharable, up-front initialization for JAXB. All configuration
must be done on individual, non-sharable JAXBContext objects. However,
JAXB does have a package-wide annotation that the other frameworks
do not. The following example shows a package-info.java
file that
contains annotations to be applied to every class in the same Java package.
//package-info.java
@XmlSchema(namespace = "urn:ejava.svc-controllers.quotes")
package info.ejava.examples.svc.content.quotes.dto;
import jakarta.xml.bind.annotation.XmlSchema;
The same feature could be used to globally apply adapters package-wide.
//package-info.java
@XmlSchema(namespace = "urn:ejava.svc-controllers.quotes")
@XmlJavaTypeAdapter(type= LocalDate.class, value=JaxbTimeAdapters.LocalDateJaxbAdapter.class)
package info.ejava.examples.svc.content.quotes.dto;
import jakarta.xml.bind.annotation.XmlSchema;
import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import java.time.LocalDate;
7.2.3. JAXB Marshalling/Unmarshalling
import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.JAXBException;
import jakarta.xml.bind.Marshaller;
import jakarta.xml.bind.Unmarshaller;
Marshalling/Unmarshalling starts out by constructing a JAXBContext
scoped to handle the classes of interest.
This will include the classes explicitly named and the classes they reference.
Therefore, one would only need to create a JAXBContext
by explicitly naming the input and return types of a Web API method.
public <T> String marshal(T object) throws JAXBException {
JAXBContext jbx = JAXBContext.newInstance(object.getClass()); (1)
Marshaller marshaller = jbx.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); (2)
StringWriter buffer = new StringWriter();
marshaller.marshal(object, buffer);
return buffer.toString();
}
1 | explicitly name primary classes of interest |
2 | adds newline and indentation formatting |
public <T> T unmarshal(Class<T> type, String buffer) throws JAXBException {
JAXBContext jbx = JAXBContext.newInstance(type);
Unmarshaller unmarshaller = jbx.createUnmarshaller();
ByteArrayInputStream bis = new ByteArrayInputStream(buffer.getBytes());
T result = (T) unmarshaller.unmarshal(bis);
return result;
}
7.2.4. JAXB Maven Aspects
Modules that define DTO classes only will require a direct dependency on the
jakarta.xml-bind-api
library for annotations and interfaces.
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
</dependency>
Modules marshalling/unmarshalling DTO classes using JAXB will require a dependency
on the jaxb-runtime
artifact.
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
</dependency>
Deprecated javax.xml.bind Dependencies
The
The following two artifacts contain the deprecated
|
8. Configure Server-side Jackson
8.1. Dependencies
Jackson JSON will already be on the classpath when using spring-boot-web-starter
.
To also support XML, make sure the server has an additional jackson-dataformat-xml
dependency.
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
8.2. Configure ObjectMapper
Both XML and JSON mappers are instances of ObjectMapper. To configure their use in our application — we can go one step higher and create a builder for jackson to use as its base. That is all we need to know as long as we can configure them identically.
Jackson’s AutoConfiguration provides a layered approach to customizing the marshaller. One can configure using:
-
spring.jackson properties (e.g.,
spring.jackson.serialization.*
) -
Jackson2ObjectMapperBuilderCustomizer — a functional interface that will be passed a builder pre-configured using properties
Assigning a high precedence order to the customizer will allow properties to flexibly override the Java code configuration.
...
import com.fasterxml.jackson.databind.SerializationFeature;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
@SpringBootApplication
public class QuotesApplication {
public static void main(String...args) {
SpringApplication.run(QuotesApplication.class, args);
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE) (2)
public Jackson2ObjectMapperBuilderCustomizer jacksonMapper() {
return (builder) -> { builder (1)
//spring.jackson.serialization.indent-output=true
.featuresToEnable(SerializationFeature.INDENT_OUTPUT)
//spring.jackson.serialization.write-dates-as-timestamps=false
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
//spring.jackson.date-format=info.ejava.examples.svc.content.quotes.dto.ISODateFormat
.dateFormat(new ISODateFormat());
};
}
1 | returns a lambda function that is called with a Jackson2ObjectMapperBuilder to customize.
Jackson uses this same definition for both XML and JSON mappers |
2 | highest order precedence applies this configuration first, then properties — allowing for overrides using properties |
8.3. Controller Properties
We can register what MediaTypes each method supports by adding a set of consumes
and produces properties to the @RequestMapping
annotation in the controller.
This is an array of MediaType values (e.g., ["application/json", "application/xml"]
)
that the endpoint should either accept or provide in a response.
@RequestMapping(path= QUOTES_PATH,
method= RequestMethod.POST,
consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE},
produces = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE})
public ResponseEntity<QuoteDTO> createQuote(@RequestBody QuoteDTO quote) {
QuoteDTO result = quotesService.createQuote(quote);
URI uri = ServletUriComponentsBuilder.fromCurrentRequestUri()
.replacePath(QUOTE_PATH)
.build(result.getId());
ResponseEntity<QuoteDTO> response = ResponseEntity.created(uri)
.body(result);
return response;
}
The Content-Type
request header is matched with one of the types listed in consumes
.
This is a single value and the following example uses an application/json
Content-Type
and the server uses our Jackson JSON configuration and DTO mappings to turn the JSON string
into a POJO.
POST http://localhost:64702/api/quotes
sent: [Accept:"application/xml", Content-Type:"application/json", Content-Length:"108"]
{
"id" : 0,
"author" : "Tricia McMillan",
"text" : "Earth: Mostly Harmless",
"date" : "1991-05-11"
}
If there is a match between Content-Type and consumes, the provider will map the body contents to the input type using the mappings we reviewed earlier. If we need more insight into the request headers — we can change the method mapping to accept a RequestEntity and obtain the headers from that object.
@RequestMapping(path= QUOTES_PATH,
method= RequestMethod.POST,
consumes={MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE},
produces={MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE})
// public ResponseEntity<QuoteDTO> createQuote(@RequestBody QuoteDTO quote) {
public ResponseEntity<QuoteDTO> createQuote(RequestEntity<QuoteDTO> request) {(1)
QuoteDTO quote = request.getBody();
log.info("CONTENT_TYPE={}", request.getHeaders().get(HttpHeaders.CONTENT_TYPE));
log.info("ACCEPT={}", request.getHeaders().get(HttpHeaders.ACCEPT));
QuoteDTO result = quotesService.createQuote(quote);
1 | injecting raw input RequestEntity versus input payload to inspect header properties |
The log statements at the start of the methods output the following two lines with request header information.
QuotesController#createQuote:38 CONTENT_TYPE=[application/json;charset=UTF-8]
QuotesController#createQuote:39 ACCEPT=[application/xml]
Whatever the service returns (success or error), the Accept
request header is
matched with one of the types listed in the produces
. This is a list of N
values listed in priority order. In the following example, the client used an
application/xml
Accept header and the server converted it to XML using our Jackson XML
configuration and mappings to turn the POJO into an XML response.
sent: [Accept:"application/xml", Content-Type:"application/json", Content-Length:"108"]
rcvd: [Location:"http://localhost:64702/api/quotes/1", Content-Type:"application/xml", Transfer-Encoding:"chunked", Date:"Fri, 05 Jun 2020 19:44:25 GMT", Keep-Alive:"timeout=60", Connection:"keep-alive"]
<quote xmlns="urn:ejava.svc-controllers.quotes" id="1">
<author xmlns="">Tricia McMillan</author>
<text xmlns="">Earth: Mostly Harmless</text>
<date xmlns="">1991-05-11</date>
</quote>
If there is no match between Content-Type and consumes, a 415
/Unsupported Media Type
error status is returned.
If there is no match between Accept and produces, a 406
/Not Acceptable
error status is returned. Most of this
content negotiation and data marshalling/unmarshalling is hidden from the controller.
9. Client Marshall Request Content
If we care about the exact format our POJO is marshalled to or the format the service returns,
we can no longer pass a naked POJO to the client library. We must wrap the POJO in a
RequestEntity
and supply a set of headers with format specifications. The following shows
an example using RestTemplate.
RequestEntity<QuoteDTO> request = RequestEntity.post(quotesUrl) (1)
.contentType(contentType) (2)
.accept(acceptType) (3)
.body(validQuote);
ResponseEntity<QuoteDTO> response = restTemplate.exchange(request, QuoteDTO.class);
1 | create a POST request with client headers |
2 | express desired Content-Type for the request |
3 | express Accept types for the response |
The following example shows the request and reply information exchange for an application/json
Content-Type and Accept header.
POST http://localhost:49252/api/quotes, returned CREATED/201
sent: [Accept:"application/json", Content-Type:"application/json", Content-Length:"146"]
{
"id" : 0,
"author" : "Zarquon",
"text" : "Whatever your tastes, Magrathea can cater for you. We are not proud.",
"date" : "1920-08-17"
}
rcvd: [Location:"http://localhost:49252/api/quotes/1", Content-Type:"application/json", Transfer-Encoding:"chunked", Date:"Fri, 05 Jun 2020 20:17:35 GMT", Keep-Alive:"timeout=60", Connection:"keep-alive"]
{
"id" : 1,
"author" : "Zarquon",
"text" : "Whatever your tastes, Magrathea can cater for you. We are not proud.",
"date" : "1920-08-17"
}
The following example shows the request and reply information exchange for an application/xml
Content-Type and Accept header.
POST http://localhost:49252/api/quotes, returned CREATED/201
sent: [Accept:"application/xml", Content-Type:"application/xml", Content-Length:"290"]
<quote xmlns="urn:ejava.svc-controllers.quotes" id="0">
<author xmlns="">Humma Kavula</author>
<text xmlns="">In the beginning, the Universe was created. This has made a lot of people very angry and been widely regarded as a bad move.</text>
<date xmlns="">1942-03-03</date>
</quote>
rcvd: [Location:"http://localhost:49252/api/quotes/4", Content-Type:"application/xml", Transfer-Encoding:"chunked", Date:"Fri, 05 Jun 2020 20:17:35 GMT", Keep-Alive:"timeout=60", Connection:"keep-alive"]
<quote xmlns="urn:ejava.svc-controllers.quotes" id="4">
<author xmlns="">Humma Kavula</author>
<text xmlns="">In the beginning, the Universe was created. This has made a lot of people very angry and been widely regarded as a bad move.</text>
<date xmlns="">1942-03-03</date>
</quote>
10. Client Filters
The runtime examples above showed HTTP traffic and marshalled payloads. That can be very convenient for debugging purposes. There are two primary ways of examining marshalled payloads.
- Switch accepted Java type to String
-
Both our client and controller declare they expect a
QuoteDTO.class
to be the response. That causes the provider to map the String into the desired type. If the client or controller declared they expected aString.class
, they would receive the raw payload to debug or later manually parse using direct access to the unmarshalling code. - Add a filter
-
Both RestTemplate and WebClient accept filters in the request and response flow. RestTemplate is easier and more capable to use because of its synchronous behavior. We can register a filter to get called with the full request and response in plain view — with access to the body — using RestTemplate. WebClient, with its asynchronous design has a separate request and response flow with no easy access to the payload.
10.1. RestTemplate and RestClient
The following code provides an example of a filter that will work for the synchronous RestTemplate and RestClient.
It shows the steps taken to access the request and response payload.
Note that reading the body of a request or response is commonly a read-once restriction.
The ability to read the body multiple times will be taken care of within the @Bean
factory method registering this filter.
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
...
public class RestTemplateLoggingFilter implements ClientHttpRequestInterceptor {
public ClientHttpResponse intercept(HttpRequest request, byte[] body,(1)
ClientHttpRequestExecution execution) throws IOException {
ClientHttpResponse response = execution.execute(request, body); (1)
HttpMethod method = request.getMethod();
URI uri = request.getURI();
HttpStatusCode status = response.getStatusCode();
String requestBody = new String(body);
String responseBody = this.readString(response.getBody());
//... log debug
return response;
}
private String readString(InputStream inputStream) { ... }
...
}
1 | interceptor has access to the client request and response |
RestTemplateLoggingFilter is for all Synchronous Requests
The example class is called RestTemplateLoggingFilter because RestTemplate was here first, the filter is used many times in many examples, and I did not want to make the generalized name change at this time.
It is specific to synchronous requests, which includes RestClient .
|
The following code shows an example of a @Bean
factory that creates RestTemplate
instances configured with the debug logging filter shown above.
@Bean
ClientHttpRequestFactory requestFactory() {
return new SimpleClientHttpRequestFactory(); (3)
}
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder,
ClientHttpRequestFactory requestFactory) { (3)
return builder.requestFactory(
//used to read the streams twice -- so we can use the logging filter
()->new BufferingClientHttpRequestFactory(requestFactory)) (2)
.interceptors(new RestTemplateLoggingFilter()) (1)
.build();
}
@Bean
public RestClient restClient(RestClient.Builder builder,
ClientHttpRequestFactory requestFactory) { (3)
return builder //requestFactory used to read stream twice
.requestFactory(new BufferingClientHttpRequestFactory(requestFactory)) (2)
.requestInterceptor(new RestTemplateLoggingFilter()) (1)
.build();
}
1 | the overall intent of this @Bean factory is to register the logging filter |
2 | must configure client with a buffer (BufferingClientHttpRequestFactory ) for body to enable multiple reads |
3 | providing a ClientRequestFactory to be forward-ready for SSL communications |
10.2. WebClient
The following code shows an example request and response filter. They are independent and are implemented using a Java 8 lambda function. You will notice that we have no easy access to the request or response body.
package info.ejava.examples.common.webflux;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
...
public class WebClientLoggingFilter {
public static ExchangeFilterFunction requestFilter() {
return ExchangeFilterFunction.ofRequestProcessor((request) -> {
//access to
//request.method(),
//request.url(),
//request.headers()
return Mono.just(request);
});
}
public static ExchangeFilterFunction responseFilter() {
return ExchangeFilterFunction.ofResponseProcessor((response) -> {
//access to
//response.statusCode()
//response.headers().asHttpHeaders())
return Mono.just(response);
});
}
}
The code below demonstrates how to register custom filters for injected WebClient instances.
@Bean
public WebClient webClient(WebClient.Builder builder) {
return builder
.filter(WebClientLoggingFilter.requestFilter())
.filter(WebClientLoggingFilter.responseFilter())
.build();
}
11. Date/Time Lenient Parsing and Formatting
In our quote example, we had an easy LocalDate to format and parse, but that even required a custom adapter for JAXB. Integration of other time-based properties can get more involved as we get into complete timestamps with timezone offsets. So lets try to address the issues here before we complete the topic on content exchange.
The primary time-related issues we can encounter include:
Potential Issue | Description |
---|---|
type not supported |
We have already encountered that with JAXB and solved using a custom adapter. Each of the providers offer their own form of adapter (or serializer/deserializer), so we have a good headstart on how to solve the hard problems. |
non-UTC ISO offset style supported |
There are at least four or more expressions of a timezone offset (Z, +00, +0000, or +00:00) that could be used. Not all of them can be parsed by each provider out-of-the-box. |
offset versus extended offset zone formatting |
There are more verbose styles (Z[UTC]) of expressing timezone offsets that include the ZoneId |
fixed width or truncated |
Are all fields supplied at all times even when they are 0 (e.g., |
We should always strive for:
-
consistent (ISO) standard format to marshal time-related fields
-
leniently parsing as many formats as possible
Let’s take a look at establishing an internal standard, determining which providers violate that standard, how to adjust them to comply with our standard, and how to leniently parse many formats with the Jackson parser since that will be our standard provider for the course.
11.1. Out of the Box Time-related Formatting
Out of the box, I found the providers marshalled OffsetDateTime
and Date
with the following format.
I provided an OffsetDateTime
and Date
timestamp with varying number of nanoseconds (123456789, 1, and 0 ns) and timezone UTC and -05:00) and the following table shows what was marshalled for the DTO.
Provider | OffsetDateTime | Trunc | Date | Trunc |
---|---|---|---|---|
Jackson |
1776-07-04T00:00:00.123456789Z 1776-07-04T00:00:00.1Z 1776-07-04T00:00:00Z 1776-07-03T19:00:00.123456789-05:00 1776-07-03T19:00:00.1-05:00 1776-07-03T19:00:00-05:00 |
Yes |
1776-07-04T00:00:00.123+00:00 1776-07-04T00:00:00.100+00:00 1776-07-04T00:00:00.000+00:00 |
No |
JSON-B |
1776-07-04T00:00:00.123456789Z 1776-07-04T00:00:00.1Z 1776-07-04T00:00:00Z 1776-07-03T19:00:00.123456789-05:00 1776-07-03T19:00:00.1-05:00 1776-07-03T19:00:00-05:00 |
Yes |
1776-07-04T00:00:00.123Z[UTC] 1776-07-04T00:00:00.1Z[UTC] 1776-07-04T00:00:00Z[UTC] |
Yes |
JAXB |
(not supported/ custom adapter required) |
n/a |
1776-07-03T19:00:00.123-05:00 1776-07-03T19:00:00.100-05:00 1776-07-03T19:00:00-05:00 |
Yes/ No |
Jackson and JSON-B — out of the box — use an ISO format that truncates
nanoseconds and uses "Z" and "+00:00" offset styles for java.time
types.
JAXB does not support java.time
types. When a non-UTC time is supplied,
the time is expressed using the targeted offset. You will notice that
Date is always modified to be UTC.
Jackson Date format is a fixed length, no truncation, always expressed
at UTC with an +HH:MM
expressed offset. JSON-B and JAXB Date formats
truncate milliseconds/nanoseconds. JSON-B uses extended timezone offset (Z[UTC]
) and JAXB
uses "+00:00" format. JAXB also always expresses the Date in EST
in my case.
11.2. Out of the Box Time-related Parsing
To cut down on our choices, I took a look at which providers out-of-the-box could parse the different timezone offsets. To keep things sane, my detailed focus was limited to the Date field. The table shows that each of the providers can parse the "Z" and "+00:00" offset format. They were also able to process variable length formats when faced with less significant nanosecond cases.
Provider | ISO Z | ISO +00 | ISO +0000 | ISO +00:00 | ISO Z[UTC] |
---|---|---|---|---|---|
Jackson |
Yes |
Yes |
Yes |
Yes |
No |
JSON-B |
Yes |
No |
No |
Yes |
Yes |
JAXB |
Yes |
No |
No |
Yes |
No |
The testing results show that timezone expressions "Z" or "+00:00" format should be portable and something to target as our marshalling format.
-
Jackson - no output change
-
JSON-B - requires modification
-
JAXB - requires no change
11.3. JSON-B DATE_FORMAT Option
We can configure JSON-B time-related field output using a java.time
format string.
java.time
permits optional characters. java.text
does not. The following expression
is good enough for Date output but will create a parser that is intolerant of varying
length timestamps. For that reason, I will not choose the type of option that locks
formatting with parsing.
JsonbConfig config=new JsonbConfig()
.setProperty(JsonbConfig.DATE_FORMAT, "yyyy-MM-dd'T'HH:mm:ss[.SSS][XXX]") (1)
.setProperty(JsonbConfig.FORMATTING, true);
builder = JsonbBuilder.create(config);
1 | a fixed formatting and parsing candidate option rejected because of parsing intolerance |
11.4. JSON-B Custom Serializer Option
A better JSON-B solution would be to create a serializer — independent of deserializer — that takes care of the formatting.
public class DateJsonbSerializer implements JsonbSerializer<Date> {
@Override
public void serialize(Date date, JsonGenerator generator, SerializationContext serializationContext) {
generator.write(DateTimeFormatter.ISO_INSTANT.format(date.toInstant()));
}
}
We add @JsonbTypeSerializer
annotation to the field we need to customize
and supply the class for our custom serializer.
@JsonbTypeSerializer(JsonbTimeSerializers.DateJsonbSerializer.class)
private Date date;
With the above annotation in place and the JsonConfig unmodified, we get output format we want from JSON-B without impacting its built-in ability to parse various time formats.
-
1776-07-04T00:00:00.123Z
-
1776-07-04T00:00:00.100Z
-
1776-07-04T00:00:00Z
11.5. Jackson Lenient Parser
All those modifications shown so far are good, but we would also like to have lenient
input parsing — possibly more lenient than built into the providers. Jackson provides
the ability to pass in a SimpleDateFormat
format string or an instance of class
that extends DateFormat
. SimpleDateFormat
does not make a good lenient parser,
therefore I created a lenient parser that uses DateTimeFormatter framework and plugged
that into the DateFormat
framework.
public class ISODateFormat extends DateFormat implements Cloneable {
public static final DateTimeFormatter UNMARSHALLER = new DateTimeFormatterBuilder()
//...
.toFormatter();
public static final DateTimeFormatter MARSHALLER = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
public static final String MARSHAL_ISO_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss[.SSS]XXX";
@Override
public Date parse(String source, ParsePosition pos) {
OffsetDateTime odt = OffsetDateTime.parse(source, UNMARSHALLER);
pos.setIndex(source.length()-1);
return Date.from(odt.toInstant());
}
@Override
public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition pos) {
ZonedDateTime zdt = ZonedDateTime.ofInstant(date.toInstant(), ZoneOffset.UTC);
MARSHALLER.formatTo(zdt, toAppendTo);
return toAppendTo;
}
@Override
public Object clone() {
return new ISODateFormat(); //we have no state to clone
}
}
I have built the lenient parser using the Java interface to DateTimeFormatter. It is designed to
-
handle variable length time values
-
different timezone offsets
-
a few different timezone offset expressions
public static final DateTimeFormatter UNMARSHALLER = new DateTimeFormatterBuilder()
.parseCaseInsensitive()
.append(DateTimeFormatter.ISO_LOCAL_DATE)
.appendLiteral('T')
.append(DateTimeFormatter.ISO_LOCAL_TIME)
.parseLenient()
.optionalStart().appendOffset("+HH", "Z").optionalEnd()
.optionalStart().appendOffset("+HH:mm", "Z").optionalEnd()
.optionalStart().appendOffset("+HHmm", "Z").optionalEnd()
.optionalStart().appendLiteral('[').parseCaseSensitive()
.appendZoneRegionId()
.appendLiteral(']').optionalEnd()
.parseDefaulting(ChronoField.OFFSET_SECONDS,0)
.parseStrict()
.toFormatter();
An instance of my ISODateFormat
class is then registered with the provider
to use on all interfaces.
mapper = new Jackson2ObjectMapperBuilder()
.featuresToEnable(SerializationFeature.INDENT_OUTPUT)
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.dateFormat(new ISODateFormat()) (1)
.createXmlMapper(false)
.build();
1 | registering a global time formatter for Dates |
In the server, we can add that same configuration option to our builder @Bean
factory.
@Bean
public Jackson2ObjectMapperBuilderCustomizer jacksonMapper() {
return (builder) -> { builder
.featuresToEnable(SerializationFeature.INDENT_OUTPUT)
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.dateFormat(new ISODateFormat()); (1)
};
}
1 | registering a global time formatter for Dates for JSON and XML |
At this point we have the insights into time-related issues and knowledge of how we can correct.
12. Summary
In this module we:
-
introduces the DTO pattern and contrasted it with the role of the Business Object
-
implemented a DTO class with several different types of fields
-
mapped our DTOs to/from a JSON and XML document using multiple providers
-
configured data mapping providers within our server
-
identified integration issues with time-related fields and learned how to create custom adapters to help resolve issues
-
learned how to implement client filters
-
took a deeper dive into time-related formatting issues in content and ways to address