Create a custom JSON marshaller

http4k Core

Gradle setup

dependencies {
    
    implementation(platform("org.http4k:http4k-bom:5.33.1.0"))

    implementation("org.http4k:http4k-core")
    implementation("org.http4k:http4k-format-jackson")
}

Custom auto-mapping JSON configurations

http4k declares an extended set of “primitive” types which it can marshall out of the box - this includes the various http4k primitives (Uri, Status), as well as a bunch of common types from the JDK such as the DateTime classes and Exceptions. These primitives types cannot be marshalled as top-level JSON structures on their own so should be contained in a custom wrapper class before transmission.

You can declare your own custom marshaller by reimplementing the Json instance and adding mappings for your own types - either uni or bi-directional.

This ability to render custom types through different JSON marshallers allows API users to provide different “views” for different purposes - for example we may wish to hide the values of some fields in the output, as below:

Example - Representing MicroTypes/TinyTypes as Strings in JSON

MicroTypes (aka Tiny Types) are popular in Kotlin providing type-safety throughout a codebase, ensuring amongst other things that method parameters are not accidentally permuted. An example of a simple microtype might be:

data class CustomerName(val value: String)
data class Customer(val name: CustomerName)

Using the standard mapper, a Customer “Bob”, would be represented as the json

Customer(name = CustomerName("Bob"))
{
    "name": {
        "value": "Bob"
    }
}

However, it might be preferable to represent CustomerName as a plain string:

{
    "name": "Bob"
}

To achieve this, there are a few simple steps - this example uses Jackson, but there are equivalent configuration schemes for the other supported JSON libraries

  1. Use the http4k ConfigurableJackson to get a base configuration
object MyJackson : ConfigurableJackson(
    // to be filled in
) 
  1. Modify it to meet your needs, registering type adapters for your types
object MyJackson : ConfigurableJackson(
    KotlinModule.Builder.Build()              // register kotlin types
        .asConfigurable()
        .withStandardMappings()               // http4k out-of-the box extras
        .text(::CustomerName, CustomerName::value)    // here is the registration of custom type
        // .text(...) - repeat the registration for each type
        .done()
        .deactivateDefaultTyping()  // other Jackson config...
        .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
)
  1. Reference this configuration in your code - particularly where using the Body.auto<xxx> pattern
import content.`how-to`.create_a_custom_json_marshaller.MyJackson.auto

val lens = Body.auto<Customer>().toLens()  // ... continue as before

Example - Representing MicroTypes using Values4k as Strings in JSON

This example uses value types from Values4k

Firstly, define a value type using the standard values4k mechanism - note that the companion object extends ValueFactory - this will be referenced in the type adapter later. The ValueFactory also provides a number of convenience methods CustomerName.of(), parse(), unwrap(), and a mechanism to validate the format of strings - very convenient to ensure that values are semantically valid throughout the entire system.

class CustomerName(value: String) : StringValue(value) {
    companion object : StringValueFactory<CustomerName>(::CustomerName)
}

Then, define a ConfigurableJackson (Moshi…) with a type adaptor for your type

object MyJackson : ConfigurableJackson(
    KotlinModule.Builder.Build()
        .asConfigurable()
        .withStandardMappings()
        .value(CustomerName) // this references the CustomerName companion object
        // .value(...) - repeat the registration for each type
        .done()
        .deactivateDefaultTyping()  // other Jackson config...
        .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
)

A full worked example is shown below.

Code

package content.howto.create_a_custom_json_marshaller

import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.module.kotlin.KotlinModule
// this import is important so you don't pick up the standard auto method!
import content.howto.create_a_custom_json_marshaller.MyJackson.auto
import org.http4k.core.Body
import org.http4k.core.Response
import org.http4k.core.Status.Companion.OK
import org.http4k.core.with
import org.http4k.format.ConfigurableJackson
import org.http4k.format.asConfigurable
import org.http4k.format.text
import org.http4k.format.withStandardMappings

object MyJackson : ConfigurableJackson(
    KotlinModule.Builder().build()
        .asConfigurable()
        .withStandardMappings()
        // declare custom mapping for our own types - this one represents our type as a
        // simple String
        .text(::PublicType, PublicType::value)
        // ... and this one shows a masked value and cannot be deserialised
        // (as the mapping is only one way)
        .text(SecretType::toString)
        .done()
        .deactivateDefaultTyping()
        .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
)

data class PublicType(val value: String)
data class SecretType(val value: String) {
    override fun toString(): String {
        return "****"
    }
}

data class MyType(val public: PublicType, val hidden: SecretType)

fun main() {
    println(
        Response(OK).with(
            Body.auto<MyType>().toLens() of MyType(
                PublicType("hello"),
                SecretType("secret")
            )
        )
    )

    /** Prints:

    HTTP/1.1 200 OK
    content-type: application/json; charset=utf-8

    {"public":"hello","hidden":"****"}

     */
}