June 14, 2018

Groovy Goodness: Customizing JSON Output

Groovy 2.5.0 adds the possibility to customize JSON output via a JsonGenerator instance. The easiest way to turn an object into a JSON string value is via JsonOutput.toJson. This method uses a default JsonGenerator with sensible defaults for JSON output. But we can customize this generator and create JSON output using the custom generator. To create a custom generator we use a builder accessible via JsonGenerator.Options. Via a fluent API we can for example ignore fields with null values in the output, change the date format for dates and ignore fields by their name or type of the value. And we can add a custom converter for types via either an implementation of the conversion as Closure or implementation of the JsonGenerator.Converter interface. To get the JSON string we simple invoke the toJson method of our generator.

In the following example Groovy code we have a Map with data and we want to convert it to JSON. First we use the default generator and then we create our own to customize the JSON output:

// Sample class to be used in JSON.
@groovy.transform.TupleConstructor
class Student { 
    String firstName, lastName
}

def data = 
    [student: new Student('Hubert', 'Klein Ikkink'),
     dateOfBirth: Date.parse('yyyyMMdd', '19730709'),
     website: 'https://www.mrhaki.com'.toURL(),
     password: 'IamSecret',
     awake: Optional.empty(),
     married: Optional.of(true), 
     location: null,
     currency: '\u20AC' /* Unicode EURO */]
     

import groovy.json.JsonGenerator
import groovy.json.JsonGenerator.Converter
        
// Default JSON generator. This generator is used by
// Groovy to create JSON if we don't specify our own. 
// For this example we define the default generator 
// explicitly to see the default output.       
def jsonDefaultOutput = new JsonGenerator.Options().build()
        
// Use generator to create JSON string.
def jsonDefaultResult = jsonDefaultOutput.toJson(data) // Or use JsonOutput.toJson(data)

assert jsonDefaultResult == '{"student":{"firstName":"Hubert","lastName":"Klein Ikkink"},' + 
    '"dateOfBirth":"1973-07-08T23:00:00+0000","website":"https://www.mrhaki.com","password":"IamSecret",' + 
    '"awake":{"present":false},"married":{"present":true},"location":null,"currency":"\\u20ac"}'


// Define custom rules for JSON that will be generated.
def jsonOutput = 
    new JsonGenerator.Options()
        .excludeNulls()  // Do not include fields with value null.
        .dateFormat('EEEE dd-MM-yyyy', new Locale('nl', 'NL')) // Set format for dates.
        .timezone('Europe/Amsterdam') // Set timezone to be used for formatting dates.
        .excludeFieldsByName('password')  // Exclude fields with given name(s). 
        .excludeFieldsByType(URL)  // Exclude fields of given type(s).
        .disableUnicodeEscaping()  // Do not escape UNICODE.
        .addConverter(Optional) { value -> value.orElse('UNKNOWN') } // Custom converter for given type defined as Closure.
        .addConverter(new Converter() {  // Custom converter implemented via Converter interface.
        
            /**
             * Indicate which type this converter can handle.
             */
            boolean handles(Class<?> type) { 
                return Student.isAssignableFrom(type)
            }
            
            /**
             * Logic to convert Student object.
             */
            Object convert(Object student, String key) {
                "$student.firstName $student.lastName"
            }
            
        })
        .build()  // Create the converter instance.

// Use generator to create JSON from Map data structure.
def jsonResult = jsonOutput.toJson(data)

assert jsonResult == '{"student":"Hubert Klein Ikkink",' + 
    '"dateOfBirth":"maandag 09-07-1973",' + 
    '"awake":"UNKNOWN","married":true,"currency":"€"}'

The JsonBuilder and StreamingJsonBuilder classes now also support the use of a JsonGenerator instance. The generator is used when the JSON output needs to be created. The internal data structure of the builder is not altered by using a custom generator.

In the following example we use the custom generator of the previous example and apply it with a JsonBuilder and StreamingJsonBuilder instance:

import groovy.json.JsonBuilder

// We can use a generator instance as constructor argument
// for JsonBuilder. The generator is used when we create the
// JSON string. It will not effecct the internal JSON data structure.
def jsonBuilder = new JsonBuilder(jsonOutput)
jsonBuilder {
    student new Student('Hubert', 'Klein Ikkink')
    dateOfBirth Date.parse('yyyyMMdd', '19730709')
    website 'https://www.mrhaki.com'.toURL()
    password 'IamSecret'
    awake Optional.empty()
    married Optional.of(true)
    location null
    currency  '\u20AC' 
}

def jsonBuilderResult = jsonBuilder.toString()

assert jsonBuilderResult == '{"student":"Hubert Klein Ikkink",' + 
    '"dateOfBirth":"maandag 09-07-1973",' + 
    '"awake":"UNKNOWN","married":true,"currency":"€"}'

// The internal structure is unaffected by the generator.
assert jsonBuilder.content.password == 'IamSecret'
assert jsonBuilder.content.website.host == 'www.mrhaki.com'


import groovy.json.StreamingJsonBuilder

new StringWriter().withWriter { output -> 

    // As with JsonBuilder we can provide a custom generator via
    // the constructor for StreamingJsonBuilder.
    def jsonStreamingBuilder = new StreamingJsonBuilder(output, jsonOutput)
    jsonStreamingBuilder {
        student new Student('Hubert', 'Klein Ikkink')
        dateOfBirth Date.parse('yyyyMMdd', '19730709')
        website 'https://www.mrhaki.com'.toURL()
        password 'IamSecret'
        awake Optional.empty()
        married Optional.of(true)
        location null
        currency  '\u20AC' 
    }

    def jsonStreamingBuilderResult = output.toString()
    
    assert jsonStreamingBuilderResult == '{"student":"Hubert Klein Ikkink",' + 
        '"dateOfBirth":"maandag 09-07-1973",' + 
        '"awake":"UNKNOWN","married":true,"currency":"€"}'
}

Written with Groovy 2.5.0.