JSON handling

Installation (Gradle)

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

    // Argo:  
    implementation("org.http4k:http4k-format-argo")

    // Gson:  
    implementation("org.http4k:http4k-format-gson")

    // Jackson: 
    implementation("org.http4k:http4k-format-jackson")

    // Klaxon: 
    implementation("org.http4k:http4k-format-klaxon")

    // KondorJson: 
    implementation("org.http4k:http4k-format-kondor-json")

    // Moshi: 
    implementation("org.http4k:http4k-format-moshi")

    // KotlinX Serialization: 
    implementation("org.http4k:http4k-format-kotlinx-serialization")
}

About

These modules add the ability to use JSON as a first-class citizen when reading from and to HTTP messages. Each implementation adds a set of standard methods and extension methods for converting common types into native JSON/XML objects, including custom Lens methods for each library so that JSON node objects can be written and read directly from HTTP messages:

Code

package guide.reference.json

import com.fasterxml.jackson.databind.JsonNode
import org.http4k.core.Body
import org.http4k.core.Method.GET
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Status.Companion.OK
import org.http4k.core.with
import org.http4k.format.Jackson
import org.http4k.format.Jackson.asJsonArray
import org.http4k.format.Jackson.asJsonObject
import org.http4k.format.Jackson.asJsonValue
import org.http4k.format.Jackson.asPrettyJsonString
import org.http4k.format.Jackson.json
import org.http4k.format.Xml.xml
import org.w3c.dom.Node

val json = Jackson

// Extension method API:

val objectUsingExtensionFunctions: JsonNode =
    listOf(
        "thisIsAString" to "stringValue".asJsonValue(),
        "thisIsANumber" to 12345.asJsonValue(),
        "thisIsAList" to listOf(true.asJsonValue()).asJsonArray()
    ).asJsonObject()

val jsonString: String = objectUsingExtensionFunctions.asPrettyJsonString()

// Direct JSON library API:
val objectUsingDirectApi: JsonNode = json.obj(
    "thisIsAString" to json.string("stringValue"),
    "thisIsANumber" to json.number(12345),
    "thisIsAList" to json.array(listOf(json.boolean(true)))
)

// DSL JSON library API:
val objectUsingDslApi: JsonNode = json {
    obj(
        "thisIsAString" to string("stringValue"),
        "thisIsANumber" to number(12345),
        "thisIsAList" to array(listOf(boolean(true)))
    )
}

val response = Response(OK).with(
    Body.json().toLens() of json.array(
        listOf(
            objectUsingDirectApi,
            objectUsingExtensionFunctions,
            objectUsingDslApi
        )
    )
)

val xmlLens = Body.xml().toLens()

val xmlNode: Node = xmlLens(Request(GET, "").body("<xml/>"))

Auto-marshalling capabilities

Some of the message libraries (eg. GSON, Jackson, Kotlin serialization, Moshi, XML) provide the mechanism to automatically marshall data objects to/from JSON and XML using reflection.

We can use this facility in http4k to automatically marshall objects to/from HTTP message bodies using Lenses:

Code

package guide.reference.json

import org.http4k.core.Body
import org.http4k.core.Method.GET
import org.http4k.core.Request
import org.http4k.format.Jackson.auto

data class Email(val value: String)
data class Message(val subject: String, val from: Email, val to: Email)

fun main() {
    // We can use the auto method here from either Jackson, Gson or the Xml message format objects.
    // Note that the auto() method needs to be manually imported as IntelliJ won't pick it up automatically.
    val messageLens = Body.auto<Message>().toLens()

    val myMessage = Message("hello", Email("[email protected]"), Email("[email protected]"))

    // to inject the body into the message - this also works with Response
    val requestWithEmail = messageLens(myMessage, Request(GET, "/"))

    println(requestWithEmail)

// Produces:
//    GET / HTTP/1.1
//    content-type: application/json
//
//    {"subject":"hello","from":{"value":"[email protected]"},"to":{"value":"[email protected]"}}

    // to extract the body from the message - this also works with Response
    val extractedMessage = messageLens(requestWithEmail)

    println(extractedMessage)
    println(extractedMessage == myMessage)

// Produces:
//    Message(subject=hello, from=Email([email protected]), to=Email([email protected]))
//    true
}

serializing an object/class for a Response via Lens.inject() - this properly sets the Content-Type header to application/json:

import kotlinx.serialization.Serializable
import org.http4k.core.Body
import org.http4k.core.HttpHandler
import org.http4k.core.Method
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Status
import org.http4k.format.KotlinxSerialization.auto
import org.http4k.lens.BiDiBodyLens

@Serializable // required by Kotlinx.Serialization
data class Car(val brand: String, val model: String, val year: Int, val miles: Int)

// 'auto' is an extension function of each org.http4k.format.[serialization library]
// example: https://github.com/http4k/http4k/blob/master/http4k-format/kotlinx-serialization/src/main/kotlin/org/http4k/format/ConfigurableKotlinxSerialization.kt
val lensCarResponse: BiDiBodyLens<Car> =
    Body.auto<Car>().toLens() // BiDi allows for outgoing + incoming

fun main() {

    val sweetride = Car("Porsche", "911 Turbo", 1988, 45000)

    // lens.inject(object, response) serializes the object and sets content-type header to 'application/json'
    // can be used with any Serializable type (Map, List, etc)
    val app: HttpHandler =
        { _: Request -> lensCarResponse.inject(sweetride, Response(Status.OK)) }

    val request: Request = Request(Method.GET, "/")
    val response = app(request)

    println(response)
    /*
    HTTP/1.1 200 OK
    content-type: application/json; charset=utf-8

    {"brand":"Porsche","model":"911 Turbo","year":1988,"miles":45000}
    */
}

There is a utility to generate Kotlin data class code for JSON documents here. These data classes are compatible with using the Body.auto<T>() functionality.

FAQ (aka gotchas) regarding Auto-marshalling capabilities

Q. Where is the Body.auto method defined?

A. Body.auto is an extension method which is declared on the parent singleton object for each of the message libraries that supports auto-marshalling - eg. Jackson, Gson, Moshi and Xml. All of these objects are declared in the same package, so you need to add an import similar to: import org.http4k.format.Jackson.auto

Q. Using Jackson, the Data class auto-marshalling is not working correctly when my JSON fields start with capital letters

A. Because of the way in which the Jackson library works, uppercase field names are NOT supported. Either switch out to use http4k-format-gson (which has the same API), or annotate your Data class with @JsonNaming(PropertyNamingStrategy.UpperCamelCaseStrategy.class) or the fields with @JsonAlias or to get it work correctly.

Q. Using Jackson, Boolean properties with names starting with "is" do not marshall properly

A. This is due to the way in which the Jackson ObjectMapper is configured. Annotation of the fields in question should help, or using ObjectMapper.disable(MapperFeature.AUTO_DETECT_IS_GETTERS)

Q. Using Gson, the data class auto-marshalling does not fail when a null is populated in a Kotlin non-nullable field

A. This happens because http4k uses straight GSON demarshalling, of JVM objects with no-Kotlin library in the mix. The nullability generally gets checked at compile-type and the lack of a Kotlin sanity check library exposes this flaw. No current fix - apart from to use the Jackson demarshalling instead!

Q. Declared with Body.auto<List<XXX>>().toLens(), my auto-marshalled List doesn't extract properly!

A. This occurs in Moshi when serialising bare lists to/from JSON and is to do with the underlying library being lazy in deserialising objects (using LinkedHashTreeMap) ()). Use Body.auto<Array<MyIntWrapper>>().toLens() instead. Yes, it's annoying but we haven't found a way to turn if off.

Q. Using Kotlin serialization, the standard mappings are not working on my data classes.

A. This happens because http4k adds the standard mappings to Kotlin serialization as contextual serializers. This can be solved by marking the fields as @Contextual.

This can be demonstrated by the following, where you can see that the output of the auto-unmarshalling a naked JSON is NOT the same as a native Kotlin list of objects. This can make tests break as the unmarshalled list is NOT equal to the native list.

As shown, a workaround to this is to use Body.auto<Array<MyIntWrapper>>().toLens() instead, and then compare using Arrays.equal()

package guide.reference.json

import org.http4k.core.Body
import org.http4k.core.Method.GET
import org.http4k.core.Request
import org.http4k.format.Moshi.auto

data class MyIntWrapper(val value: Int)

fun main() {
    val aListLens = Body.auto<List<MyIntWrapper>>().toLens()

    val req = Request(GET, "/").body(""" [ {"value":1}, {"value":2} ] """)

    val extractedList = aListLens(req)

    val nativeList = listOf(MyIntWrapper(1), MyIntWrapper(2))

    println(nativeList)
    println(extractedList)
    println(extractedList == nativeList)

    //solution:
    val anArrayLens = Body.auto<Array<MyIntWrapper>>().toLens()

    println(anArrayLens(req).contentEquals(arrayOf(MyIntWrapper(1), MyIntWrapper(2))))

// produces:
//    [MyIntWrapper(value=1), MyIntWrapper(value=2)]
//    [{value=1}, {value=2}]
//    false
//    true
}