2 minutes read

Content negotiation refers to a process where the server and client agree on what type of data should be exchanged between them over HTTP requests/responses. This process is based on certain criteria, such as the requested media type from the client side or the supported media types from the server side. The negotiated result could be one of the formats, like JSON or XML, but it could also be a custom format like Protocol Buffers.

The content negotiation and serialization plugin enables you to easily negotiate content types between your server and client applications, as well as serialize data into different formats. In this section, we will look at how you can use this tool in your projects to work with JSON and XML formats.

Installation

To use ContentNegotiation, you need to include the ktor-server-content-negotiation artifact in the build script:

implementation("io.ktor:ktor-server-content-negotiation:$ktor_version")

Configure the XML serializer

In addition to the main artifact, you will need to connect a converter for XML:

implementation("io.ktor:ktor-serialization-kotlinx-xml:$ktor_version")

Finally, since we are using kotlinx, we will have to connect plugin.serialization:

plugins {
    ...
    kotlin("plugin.serialization") version "1.8.0"
}

Now we can install and configure the plugin:

fun Application.configureSerialization() {
    install(ContentNegotiation) {
        xml(format = XML { // XmlConfig.Builder
            xmlDeclMode = XmlDeclMode.Charset
        })
    }
}

Inside XmlConfig.Builder you can configure XML declaration style. There are four options:

  • XmlDeclMode.None: without any declaration line.
  • XmlDeclMode.Charset: emit an XML declaration that includes the character set: <?xml version="1.1" encoding="UTF-8"?>.
  • XmlDeclMode.Minimal: emit an XML declaration just containing the XML version number: <?xml version="1.1"?>.
  • XmlDeclMode.Auto: emit an XML declaration of whatever is provided by default, if possible, minimal.

Install the JSON serializer

If you want to work with JSON, then, by analogy with XML, you need to connect a converter. There are three converter implementations: kotlinx.serialization, Gson, and Jackson.

KotlinX is specifically tailored for the Kotlin language, while Gson and Jackson are both Java libraries. KotlinX is generally more concise than Gson and Jackson, as it takes advantage of Kotlin's features to make code more compact. Additionally, KotlinX supports a wider range of data types than Gson and Jackson, including Kotlin-specific data types, such as sealed classes, enums, and data classes.

To use kotlinx, you need to include the ktor-serialization-kotlinx-json artifact in the build script:

implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version")

Gson is a popular choice for Android applications, as it is optimized for mobile devices and adds minimal overhead to the application. It is also relatively easy to use and provides lots of customization options.

To use gson, you need to include the ktor-serialization-gson artifact in the build script:

implementation("io.ktor:ktor-serialization-gson:$ktor_version")

Jackson is the most powerful of the three libraries. It offers a wide range of features, including support for streaming, the tree model, and data binding. It also has a wide range of modules that can be used to customize the library to your specific needs.

To use jackson, you need to include the ktor-serialization-jackson artifact in the build script:

implementation("io.ktor:ktor-serialization-jackson:$ktor_version")

In this article, we will use the kotlinx converter, because it is also used for XML format. For this reason, do not forget to connect the plugin.serialization:

plugins {
    ...
    kotlin("plugin.serialization") version "1.8.0"
}

Configure the JSON serializer

Now we can install and configure the plugin:

fun Application.configureSerialization() {
    install(ContentNegotiation) {
        json(Json { // JsonBuilder
            ignoreUnknownKeys = true
            isLenient = true
            prettyPrint = true
        })
    }
}

Inside JsonBuilder we can configure the serializer:

  • ignoreUnknownKeys: specifies whether encounters of unknown properties in the input JSON should be ignored instead of throwing SerializationException.
  • isLenient: removes JSON specification restriction (RFC-4627) and makes the parser more liberal to the malformed input. In lenient mode, quoted boolean literals, and unquoted string literals are allowed, for example {"name": John}.
  • prettyPrint: determines whether or not JSON should be formatted with indentation and line breaks when encoding. When set to true, the encoded JSON will be formatted in a human-readable way, while when set to false, the encoded JSON will be compact and unformatted.

Write application

In this article, we will work simultaneously with JSON and XML serialization, so the configuration will look like this:

fun Application.configureSerialization() {
    install(ContentNegotiation) {
        xml(format = XML {
            xmlDeclMode = XmlDeclMode.Charset
        })

        json(Json {
            ignoreUnknownKeys = true
            isLenient = true
            prettyPrint = true
        })
    }
}

Now let's create data classes to demonstrate how the serializer works. When you work with kotlinx, make sure that the data class has the @Serializable annotation:

@Serializable
data class Movie(val title: String, val rating: Double)

// Some kind of storage
val movies = mutableSetOf<Movie>()

So, all we have to do is add the routing:

fun Application.configureRouting() {
    routing {
        route("/movies") {
            get {
                call.respond(movies)
            }

            post {
                movies += call.receive<Movie>()
                call.respond(movies)
            }
        }
    }
}

When a GET request is made to the endpoint /movies, it responds with the list of movies. When a POST request is made, it adds the provided movie to the list and then responds with the updated list.

Receive data

Since we have configured both serializers for JSON and XML, you can send a message in two formats. Ktor determines the required serializer based on the request header Content-Type.

For example, to request with XML data would look like this:

POST http://127.0.0.1:8080/movies
Content-Type: application/xml

<Movie title="Mr. Robot" rating="8.6" />

The request with JSON data looks like this:

POST http://127.0.0.1:8080/movies
Content-Type: application/json

{
    "title": "Mr. Robot",
    "rating": 8.6
}

Send data

When the server sends the data, it is guided by the Accept header. If the header is not explicitly passed, the data will be returned in the format according to the Content-Type header.

For example, such a response will come to a GET request with the header Accept: application/xml:

<?xml version="1.1" encoding="UTF-8"?>
<LinkedHashSet>
    <Movie title="Mr. Robot" rating="8.6"/>
</LinkedHashSet>

But this response will come to the request with the header Accept: application/json:

[
    {
        "title": "Mr. Robot",
        "rating": 8.6
    }
]

Conclusion

We learned that the content negotiation and serialization plugin provides an easy way to handle content types in a web application. It allows developers to easily configure both client-side and server-side content negotiations as well as serialize data into different formats.

2 learners liked this piece of theory. 0 didn't like it. What about you?
Report a typo