Using Gson with Http4k

An example code to use Gson with http4k. See beverage-buddy-jooq to see this code in action.

First, some basic utility methods for Gson:

/**
 * Parses [json] as a list of items with class [itemClass] and returns that.
 */
fun <T> Gson.fromJsonArray(json: String, itemClass: Class<T>): List<T> =
    fromJsonArray(StringReader(json), itemClass)

/**
 * Parses JSON from a [reader] as a list of items with class [itemClass] and returns that.
 */
fun <T> Gson.fromJsonArray(reader: Reader, itemClass: Class<T>): List<T> {
    val type: Type = TypeToken.getParameterized(List::class.java, itemClass).type
    return fromJson<List<T>>(reader, type)
}

/**
 * Parses the response as a JSON and converts it to a Java object.
 */
fun <T> Body.json(gson: Gson, clazz: Class<T>): T = gson.fromJson(stream.buffered().reader(), clazz)

/**
 * Parses the response as a JSON array and converts it into a list of Java object with given [clazz].
 */
fun <T> Body.jsonArray(gson: Gson, clazz: Class<T>): List<T> = gson.fromJsonArray(stream.buffered().reader(), clazz)

/**
 * Parses the response as a JSON array and converts it into a list of Java object with given [clazz].
 */
inline fun <reified T> Body.jsonArray(gson: Gson): List<T> = jsonArray(gson, T::class.java)

The client code:

private fun Response.checkOk(request: Request) {
    if (!status.successful) {
        val msg = "$request ====> $this"
        close() // close the streams in case of StreamResponse
        if (status.code == 404) throw FileNotFoundException(msg)
        throw IOException(msg)
    }
}

/**
 * Makes sure that the [Response]'s code is 200..299. Fails with an exception if it's not.
 */
val CheckOk = Filter { next -> { next(it).apply { checkOk(it) } } }

fun Request.accept(contentType: ContentType): Request = header("Accept", contentType.toHeaderValue())

fun Request.acceptJson(): Request = accept(ContentType.APPLICATION_JSON)

class PersonRestClient {
  private val client: HttpHandler = ClientFilters.SetBaseUriFrom(Uri.of("http://localhost:8080/rest"))
    .then(ClientFilters.FollowRedirects())
    .then(CheckOk)
    .then(JavaHttpClient(responseBodyMode = BodyMode.Stream))
  private val gson = RestService.gson

  fun getAllCategories(): List<Category> {
    val request = Request(Method.GET, "categories").acceptJson()
    return client(request).use { response -> response.body.jsonArray<Category>(gson) }
  }
}

The server code:

object RestService {
    val gson: Gson = GsonBuilder().registerJavaTimeAdapters().create()
    private fun Response.json(json: Any): Response =
        header("Content-Type", ContentType.APPLICATION_JSON.toHeaderValue())
            .body(gson.toJson(json))

    val app: RoutingHttpHandler = routes(
        "categories" bind GET to { Response(OK).json(db { CATEGORY.dao.findAll() }) }
    ).withBasePath("rest")
}

/**
 * Provides access to person list. To test, just run `curl http://localhost:8080/rest/categories`
 */
@WebServlet(
    urlPatterns = ["/rest/*"],
    name = "RestServlet",
    asyncSupported = false
)
class RestServlet : HttpServlet() {
    private val adapter = Http4kJakartaServletAdapter(RestService.app)
    override fun service(req: HttpServletRequest, resp: HttpServletResponse) =
        adapter.handle(req, resp)
}

private fun GsonBuilder.registerJavaTimeAdapters(): GsonBuilder = apply {
    Converters.registerAll(this)
}
Written on October 31, 2023