Introduction
dependencies: - $ktor.server.contentNegotiation - $ktor.serialization.kotlinx.jsonContent Negotiation
When you created the project, you included the Content Negotiation plugin. This plugin looks at the types of content that the client can render and matches these against the content types that the current service can provide. Hence, the term Content Negotiation.
In HTTP the client signals which content types it can render through the Accept header. The value of this header is one or more content types. In the case above you can examine the value of this header by using the development tools built into your browser.
Consider the following example:
text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7Note the inclusion of */* .This header signals that it accepts HTML, XML or Images - but it would also accept any other content type.
The Content Negotiation plugin needs to find a format to send data back to the browser. If you look inside the generated code in the project, you will find a file called Serialization.kt inside src/main/kotlin/com/example, which includes the following:
install(ContentNegotiation) { json()}This code installs the ContentNegotiation plugin, and also configures the kotlinx.serialization plugin. With this, when clients send requests, the server can send back objects serialized as JSON.
In the case of the request from the browser, the ContentNegotiation plugin knows it can only return JSON, and the browser will try to display anything it is sent. So the request succeeds.
Routes
GET
You can implement the routes for GET requests.
Differently from Ktor - HTML previous example, the code can be simplified because you no longer need to worry about converting objects to HTML:
route("/product") {
get { call.respond(products) }}POST
You will make use of the kotlinx.serialization framework which will do the majority of the heavy lifting.
route("/product") {
post { try { val product = call.receive<Product>() products[product.id] = product call.respond(HttpStatusCode.Created)
} catch (ex: IllegalStateException) { call.respond(HttpStatusCode.BadRequest) } catch (ex: SerializationException) { call.respond(HttpStatusCode.BadRequest) } }}When a POST request is sent to /product the kotlinx.serialization framework is used to convert the body of the request into a Product object:
- If this succeeds, the product will be added to the repository.
- If the deserialization process fails, the server will need to handle a
SerializationException - Whereas if the product is a duplicate, it will need to handle an
IllegalStateException.
delete
Now you are going to implement the Delete operation.
Test
ktor client
You can implement JUnit tests, using the built-in client object to fetch and deserialize JSON.
Note that you need to install the ContentNegotiation and kotlinx.serialization plugins into the Ktor client, in the same way as you did on the server.
test-dependencies: - $ktor.client.contentNegotiation@Test fun testPost() = testApplication { application { module() }
val client = createClient { install(ContentNegotiation) { json() } }
val product = Product(6, "Monitor", "27-inch 4K UHD monitor with HDR support")
// POST var response = client.post("/product") { header(HttpHeaders.ContentType, ContentType.Application.Json) setBody(product) } response.status shouldBe HttpStatusCode.Created
// GET response = client.get("/product/6") response.status shouldBe HttpStatusCode.OK response.body<Product>() shouldBe product }JsonPath
If your service is intended for use by multiple clients, it’s crucial to have confidence in the JSON structure. To achieve this, use the Ktor Client to retrieve text from the server and then analyze this content using the JSONPath library.
val json = client.get("/product/10").body<String>()JsonPath.read<String>(json, "$.name") shouldBe "Webcam"The JsonPath queries work as follows:
-
$[*].namemeans “treat the document as an array and return the value of thenameproperty of each entry.” -
$[?(@.priority == '$priority')].namemeans “return the value of thenameproperty of every entry in the array with a priority equal to the supplied value”.
You can use queries like these to confirm your understanding of the returned JSON. When you do code refactoring and service redeployment, any modifications in serialization will be identified, even if they don’t disrupt deserialization with the current framework. This allows you to republish publicly available APIs with confidence.