JSON-линзы в http4k #

Васильев Андрей Михайлович, 2024

Версии презентации

Поддержка JSON #

http4k предоставляет отличную поддержку для работы с JSON-документами:

  • Удобное конструирование JSON-документов произвольной структуры
  • Запись JSON-документов как ответы на HTTP-запросы
  • Считывание JSON-документов из тела HTTP-запросов

Причём поддержка существует для множества библиотек, в том числе и для Jackson

Для подключения данных возможностей в проект необходимо добавить зависимость в build.gradle.kts-файл или build.properties.json-файл

"org.http4k:http4k-format-jackson"

Не забудьте указать версию, совместимую с основной библиотекой http4k-core

Линзы для записи тела #

Библиотека Jackson предоставляет возможности для преобразования произвольных объектов в JSON-документы с помощью рефлексии

diagram

С использованием данного механизма можно создать линзу

  • Которая формирует JSON-документ
  • Записывает данный документ как тело объекта ответа
  • Указывает корректный тип документа в заголовках ответа

При использовании такой линзы для формирования ответа достаточно сформировать данные и применить линзу к ответу

Создание линзы #

Каждое конкретное дополнение для библиотеки предоставляет функцию auto, которую можно применить к телу запроса

import org.http4k.format.Jackson.auto

Далее необходимо специфицировать объект, который будет записан тело ответа

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

Для данного класса необходимо сформировать линзу:

val messageLens = Body.auto<Message>().toLens()

Теперь эту линзу можно применить внутри обработчика HTTP-запроса

val myMessage = Message("hello", "bob@git.com", "sue@git.com")
val response: Response = messageLens(myMessage, Response(Status.OK))

Применение нескольких линз #

Для записи значения в целевой объект линза предоставляет функцию

fun invoke(message: Message, response: Response): Response

Если мы захотим применить несколько линз к объекту-ответа, то придётся оборачивать одну линзу в другую:

firstLens(firstData, secondLens(secondData, Response(Status.OK)))

Для более красивого применения линз http4k предлагает функцию org.http4k.core.with, которой передаётся набор модификаторов

Для создания модификатора из линзы необходимо использовать функцию of

Response(Status.OK).with(
    firstLens of firstData,
    secondLens of secondData,
)
  • Функция of является инфиксной
  • Можно объединять линзы, которые работают над одним объектом

Универсальная Jackson-линза для записи #

Линза, которая создаётся с помощью функции auto, является двунаправленной, т.е. может как записывать JSON-документы, так и считывать их

Если мы ограничим сферу её применения только записью данных, то можно довериться возможностям ObjectMapper для преобразования любого объекта в тело документа

Т.е. можно создать универсальную линзу для записи данных:

val jsonBodyLens = Body.auto<Any>().toLens()

Эту линзу можно использовать с любыми данными:

val myMessage = Message("hello", "bob@git.com", "sue@git.com")
Response(Status.OK).with(
    jsonBodyLens of myMessage,
)

Создание JSON-документа из узлов #

Помимо передачи объектов существующих классов на вход линзы можно подать низкоуровневое представление JSON-документа в виде узлов, объектов JsonNode

Для создания таких объектов предоставляется удобный интерфейс

import com.fasterxml.jackson.databind.JsonNode
import org.http4k.format.Jackson
val json = Jackson
// 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)))
    )
}

Объекты, созданные таким образом, можно передать линзе

Считывание JSON-данных из запроса #

Линзу также можно применить для извлечения данных из тела запроса

  • Необходимо использовать типизированную линзу
  • В случае невозможности извлечения данных в целевой объект будет выброшено исключение LensFailure
data class Message(val subject: String, val from: String, val to: String)
val messageLens = Body.auto<Message>().toLens()

Данную линзу можно применить к объекту-запросу в обработчике HTTP-запроса

val message: Message = messageLens(request)

Данный подход не позволяет получить информацию по всем возможным ошибкам в передаваемом объекте, только о первой