Sharing Grails HAL and JSON Renderers

Details a method to share Grails object marshallers across XML, JSON and HAL flavors of web services..

Patrick Double

Grails provides nice features for creating web services, customization is usually terse. According to the “Registering Custom Objects Marshallers” section of the Grails User Guide, custom object marshallers can registered using a simple closure. However, it turns out that these marshallers do not apply if you are serving HAL (HATEOS) in addition to JSON/XML. This post will detail a method to share marshallers across XML, JSON and HAL flavors of web services.

# Create Adapter for Gson The problem happens because the HAL renderer is using Gson for marshalling which does fit the following pattern in the Grails User Guide:

[JSON, XML]*.registerObjectMarshaller(DateTime) {
    return it?.toString("yyyy-MM-dd'T'HH:mm:ss'Z'")
}

In order to include HAL in this list, we need to create an adapter class to register the closure. The adapter will then work the closure into the Gson library. Note that Gson performs input and output processing, the approach here only addresses output processing.

import com.google.gson.Gson
import com.google.gson.TypeAdapter
import com.google.gson.TypeAdapterFactory
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter

/**
 * Allows closures to be used to marshall objects when {@link Gson} is used for serialization.
 */
class ClosureTypeAdapterFactory implements TypeAdapterFactory {
  private Map<Class<?>, Closure<?>> converters = [:]

  void registerObjectMarshaller(Class<?> clazz, Closure<?> callable) {
    converters[clazz] = callable
  }

  @SuppressWarnings('UnusedMethodParameter')
  public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
    Class<T> rawType = (Class<T>) type.getRawType();
    Closure<?> callable = converters[rawType]
    if (!callable) {
      return null;
    }

    return new TypeAdapter() {
      @SuppressWarnings("CatchException")
      public void write(JsonWriter out, def value) throws IOException {
        if (value == null) {
          out.nullValue();
        } else {
          try {
            int argCount = callable.getParameterTypes().length;
            Object result;
            if (argCount == 1) {
              result = callable.call(value);
            }
            else {
              throw new IOException(
                  "Invalid Parameter count for registered Object Marshaller for class " + rawType.getName());
            }

            if (result == null) {
              out.nullValue()
            } else {
              out.value(result)
            }
          }
          catch (Exception e) {
            throw e instanceof IOException ? (IOException) e : new IOException(e);
          }
        }
      }

      public def read(JsonReader reader) throws IOException {
        if (reader.peek() == JsonToken.NULL) {
          reader.nextNull()
          return null
        } else {
          reader.nextString()
          return null
        }
      }
    }
  }
}

We need a test, right? Of course we do!

import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter
import org.springframework.context.MessageSource
import org.springframework.context.MessageSourceResolvable
import org.springframework.context.i18n.LocaleContextHolder
import org.springframework.context.support.DefaultMessageSourceResolvable
import spock.lang.Specification

class ClosureTypeAdapterFactorySpec extends Specification {
  public static final MessageSourceResolvable MESSAGE = new DefaultMessageSourceResolvable("default.boolean.true")

  ClosureTypeAdapterFactory factory
  Gson gson
  TypeToken<List<String>> listStringType
  TypeToken<MessageSourceResolvable> messageSourceResolvableType
  StringWriter stringWriter
  JsonWriter jsonWriter

  def setup() {
    factory = new ClosureTypeAdapterFactory()
    def messageSource = Stub(MessageSource)
    messageSource.getMessage(_, _) >> "resolved message"
    factory.registerObjectMarshaller(MessageSourceResolvable) {
      return messageSource.getMessage(it, LocaleContextHolder.locale)
    }

    def factoryBean = new GsonFactoryBean()
    factoryBean.closureTypeAdapterFactory = factory
    factoryBean.afterPropertiesSet()
    gson = factoryBean.object

    listStringType = new TypeToken<List<String>>() {}
    messageSourceResolvableType = new TypeToken<MessageSourceResolvable>() {}

    stringWriter = new StringWriter()
    jsonWriter = new JsonWriter(stringWriter)
    jsonWriter.beginArray()
  }

  def "handle unsupported type"() {
    expect:
    !factory.create(gson, listStringType)
  }

  def "handle supported type"() {
    expect:
    factory.create(gson, messageSourceResolvableType)
  }

  def "write null value"() {
    given:
    def typeAdapter = factory.create(gson, messageSourceResolvableType)

    when:
    typeAdapter.write(jsonWriter, null)

    then:
    stringWriter.toString() == "[null"
  }

  def "write value"() {
    given:
    def typeAdapter = factory.create(gson, messageSourceResolvableType)

    when:
    typeAdapter.write(jsonWriter, MESSAGE)

    then:
    stringWriter.toString() == "[\"resolved message\""
  }

  def "write value with null conversion"() {
    given:
    factory.registerObjectMarshaller(MessageSourceResolvable) {
      return null
    }
    def typeAdapter = factory.create(gson, messageSourceResolvableType)

    when:
    typeAdapter.write(jsonWriter, MESSAGE)

    then:
    stringWriter.toString() == "[null"
  }

  def "write value with exception"() {
    given:
    factory.registerObjectMarshaller(MessageSourceResolvable) {
      throw new UnsupportedOperationException()
    }
    def typeAdapter = factory.create(gson, messageSourceResolvableType)

    when:
    typeAdapter.write(jsonWriter, MESSAGE)

    then:
    thrown(IOException)
  }

  def "read null"() {
    given:
    def typeAdapter = factory.create(gson, messageSourceResolvableType)
    def jsonReader = Stub(JsonReader) {
      peek() >> JsonToken.NULL
    }

    expect:
    typeAdapter.read(jsonReader) == null
  }

  def "read string"() {
    given:
    def typeAdapter = factory.create(gson, messageSourceResolvableType)
    def jsonReader = Stub(JsonReader) {
      peek() >> JsonToken.STRING
      nextString() >> "resolved message"
    }

    expect:
    typeAdapter.read(jsonReader) == null
  }

  def "zero arg closure"() {
    given:
    factory.registerObjectMarshaller(MessageSourceResolvable) { ->
      "zero arg"
    }

    when:
    gson.getAdapter(MessageSourceResolvable).toJson(MESSAGE)

    then:
    thrown(IOException)
  }

  def "two arg closure"() {
    given:
    factory.registerObjectMarshaller(MessageSourceResolvable) { one, two ->
      "two arg"
    }

    when:
    gson.getAdapter(MessageSourceResolvable).toJson(MESSAGE)

    then:
    thrown(IOException)
  }
}

# Adapter Registration Now that we have an adapter we need to register it as a Spring bean to make it available in BootStrap.groovy.

beans = {
  GSONFAC(ClosureTypeAdapterFactory)
}

# Marshaller Registration Now for the part for which we’ve been waiting. These are only examples and not necessary to make this work for your own marshallers.

import grails.converters.JSON
import grails.converters.XML
import org.bson.types.ObjectId
import org.springframework.context.MessageSourceResolvable
import org.springframework.context.i18n.LocaleContextHolder

class BootStrap {
  def init = { servletContext ->
    [JSON, XML /*Gson already handles ObjectID*/]*.registerObjectMarshaller(ObjectId) {
      return it?.toString()
    }

    [JSON, XML, GSONFAC]*.registerObjectMarshaller(MessageSourceResolvable) {
      return it ? messageSource.getMessage(it, LocaleContextHolder.locale) : null
    }

    [JSON, XML, GSONFAC]*.registerObjectMarshaller(Enum) {
      it?.name()
    }
  }
}

The new GSONFAC has the registerObjectMarshaller method like JSON and XML. Using the Groovy splat operator, we can easily re-use the closure.

This was a difficult find for me, why the JSON tests were working but HAL was not. Hopefully this post will save you time and write less code!

Share this Post

Related Blog Posts

JVM

Retrofit: API Integration Made Easy

August 4th, 2015

A quick introduction to Retrofit. Well cover the basics for how to convert any API into a type-safe Java interface using @GET, @POST, @QUERY, and @PARAM.

Darin Drobinski
JVM

Creating a Mocked RESTful Sandbox Server using Groovy

June 25th, 2015

Create a mocked RESTful web service using Groovy, which allows users to test out the service without changing any data.

Object Partners
JVM

Waiting for a Redirect Chain to Settle using Selenium WebDriver

June 4th, 2015

Some interactions with the browser can be tricky. This post describes a way to wait for the document to settle before continuing.

Patrick Double

About the author

Patrick Double

Principal Technologist

I have been coding since 6th grade, circa 1986, professionally (i.e. college graduate) since 1998 when I graduated from the University of Nebraska-Lincoln. Most of my career has been in web applications using JEE. I work the entire stack from user interface to database.   I especially like solving application security and high availability problems.