How To Consume and Return Data In YAML In Jakarta REST

Photo of Luqman Saeed by Luqman Saeed

YAML is a simple, human-friendly data serialization language for all programming languages. It is the main format for working with Docker. As a language agnostic format, there are many bindings for all the major programming languages. You can easily consume and return data in the YAML format in your Jakarta REST application using message body readers and writers.

A message body reader or writer, in Jakarta REST, is an extension that reads data sent by the client or writes data generated on the server to the underlying HTTP input/output stream. The data in question, is mostly a given Java type. Readers and writers can be registered to be picked up automatically by the Jakarta REST runtime by annotating them with the jakarta.ws.rs.ext.Provider annotation. This blog post shows implementations of a body reader and writer. The Java library for interfacing with the YAML data format is SnakeYaml. Let's start by adding the SnakeYAML dependency to our code.

        <dependency>
            <groupId>org.yaml</groupId>
            <artifactId>snakeyaml</artifactId>
            <version>2.0</version>
        </dependency>

Now let's start by looking at the reader.

@Provider
@Consumes(YAML_MEDIA_TYPE)
public class CustomYamlReader<T> implements MessageBodyReader<T> {

 @Inject
 Yaml yaml;
 @Override
 public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
 return YAML_MEDIA_TYPE.equalsIgnoreCase(mediaType.toString());


 }

 @Override
 public T readFrom(Class<T> type, Type genericType, Annotation[] annotations, MediaType mediaType,
 MultivaluedMap<String, String> httpHeaders, InputStream entityStream)
 throws IOException, WebApplicationException {
 var inputString = new String(entityStream.readAllBytes());
 return yaml.loadAs(inputString, type);
 }

}

The CustomYamlReader is a generified implementation of the jakarta.ws.rs.ext.MessageBodyReader interface. It is annotated @Provider and @Consumes("application/x-yml"). This way, we associate the YAML content type to this reader implementation. This interface has two methods - isReadable and readFrom. The first method, isReadable, is called to determine if this reader can read data for the currently matched HTTP request. The above implementation checks if the jakarta.ws.rs.core.MediaType is the same as our custom media type, in this case "application/x-yaml." Returning true tells the runtime that this reader can be called to read the passed data from the underlying HTTP input stream. 

For special cases where the reader is meant to read data for a specific Java type, the isReadable method can also check to see if the passed type (first method parameter) is the same as the expected type. But in the above example, we expect to use this reader for all types in our application, so we don't do that check. You can also do other custom checks specific to your business domain. In the end, the method must return a boolean to inform the Jakarta REST runtime whether it can or cannot read the message. 

The isReadable method takes four parameters. The first one is a java.lang.Class - that is the type of the data that is being read. The second parameter is a java.lang.reflect.Type. This is generic type information about the data being read. The third parameter is an array of java.lang.annotation.Annotation objects on the matched Jakarta REST resource method. The fourth parameter is the media type that this reader can read for.

The readFrom method does the actual work of reading the passed data from the HTTP input stream. In the above implementation, we read the input stream into a string object, then pass the string to the loadAs method of the YAML instance. The YAML instance is injected rather than manually instantiated. The producer method for the YAML instance is shown below.

    @Produces
    @RequestScoped
    public Yaml getYaml() {
        return new Yaml();
    }

As the YAML instance is not thread safe, we need a new instance for each time we need to marshal data. However, creating new instances could end up polluting our code with YAML instance creation all over the place. So instead we use the Jakarta CDI producer construct to produce instances for each request, as the producer method is annotated @RequestScoped. This also allows us to centralize any customization or configuration we would want to carry out on the YAML instance at one place instead of repeating it all over. 

With the reader in place and automatically registered with the Jakarta REST runtime via the @Provider annotation, let's look at the writer.

@Provider
@Produces(CustomYamlWriter.YAML_MEDIA_TYPE)
public class CustomYamlWriter<T> implements MessageBodyWriter<T> {
 @Inject
 Yaml yaml;


 public static final String YAML_MEDIA_TYPE = "application/x-yaml";


 @Override
 public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
 return YAML_MEDIA_TYPE.equalsIgnoreCase(mediaType.toString());
 }


 @Override
 public void writeTo(T t, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType,
 MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream)
 throws IOException, WebApplicationException {
 var outputStreamWriter = new OutputStreamWriter(entityStream);
 yaml.dump(t, outputStreamWriter);
 outputStreamWriter.close();
}
}

The CustomWriter is also a generified implementation of the jakarta.ws.rs.ext.MessageBodyWriter interface. It is annotated @Provider and @Produces(CustomYamlWriter.YAML_MEDIA_TYPE). The first annotation automatically registering it with the Jakarta REST runtime, and the second annotation associating this writer the YAML media type.  This interface has three methods - isWriteable, writeTo and getSize. The isWriteable is not much different from the isReadable we saw in the reader. It's called to determine if this writer can write the given information for the matched resource method. The getSize method is called to get the size of the data to set as the Content-Length. If you don't know the size, you can leave it out. The default implementation returns -1. The underlying HTTP servlet handle that for you. 

The writeTo method is analogous to the readFrom method of the reader. It writes the passed data instance (first parameter of the method) to the output stream of the underlying HTTP request. The above implementation creates an OutputStreamWriter from the passed output stream and then calls the dump method on the YAML instance to write the transformed Java type to YAML into the output stream. 

With our reader and writer in place, we can freely declare Jakarta REST resource methods as consuming or producing YAML as shown in the following sample method.

    @POST
    @Produces(CustomYamlWriter.YAML_MEDIA_TYPE)
    @Consumes(CustomYamlWriter.YAML_MEDIA_TYPE)
    public Department save(@NotNull @Valid Department department) {
        count.inc();
        return dataController.saveDepartment(department);
    }

We can then make calls to this endpoint by setting the content-type as application/x-yml with YAML data as shown below.

POST http://localhost:8080/jee-mongo/api/department
Content-Type: application/x-yaml

departmentCode: HR-HR-05
departmentName: Finance

The server appropriately responds with the following data

POST http://localhost:8080/jee-mongo/api/department

HTTP/1.1 200 OK
Server: Payara Server 6.2023.4 #badassfish
X-Powered-By: Servlet/6.0 JSP/3.1 (Payara Server 6.2023.4 #badassfish Java/Azul Systems, Inc./17)
Content-Type: application/x-yaml
Content-Length: 124
X-Frame-Options: SAMEORIGIN

!!fish.payara.entities.Department
departmentCode: HR-161
departmentName: Finance
employees: [ ]
id: 645a56c0e48035020309676c

This is how easy it is to create your own custom data types and freely consume or return same using the Jakarta REST API. With just a few lines of code, and thanks to the SnakeYaml library, we our Jakarta REST resource methods can consume and produce data in the YAML format. Using these same constructs you can return data for many data formats that do not have default readers and writers in Jakarta REST.

If you found this useful, try some of our other Jakarta EE resources:

Comments