JSON handling
Installation (Gradle)¶
dependencies {
implementation(platform("org.http4k:http4k-bom:5.8.1.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
}