CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-io-ktor--ktor-client-json-jvm

JSON serialization plugin for Ktor HTTP client (JVM platform)

Pending
Overview
Eval results
Files

jackson-serializer.mddocs/

Jackson Serialization

Jackson-based JSON serialization implementation for Ktor HTTP clients. Provides high-performance JSON processing with extensive configuration options, custom serializers, and advanced data binding features through the Jackson ObjectMapper.

Capabilities

JacksonSerializer Class

JsonSerializer implementation using Jackson's ObjectMapper for JSON processing.

/**
 * JsonSerializer using Jackson as backend.
 */
@Deprecated("Please use ContentNegotiation plugin and its converters")
class JacksonSerializer(
    jackson: ObjectMapper = jacksonObjectMapper(),
    block: ObjectMapper.() -> Unit = {}
) : JsonSerializer {
    /**
     * Convert data object to OutgoingContent using Jackson serialization.
     */
    override fun write(data: Any, contentType: ContentType): OutgoingContent
    
    /**
     * Read content from response using Jackson deserialization.
     */
    override fun read(type: TypeInfo, body: Input): Any
}

Service Registration

The JacksonSerializer is automatically discoverable on JVM through the ServiceLoader mechanism:

META-INF/services/io.ktor.client.plugins.json.JsonSerializer

io.ktor.client.plugins.jackson.JacksonSerializer

Dependencies

To use JacksonSerializer, add the Jackson dependency to your project:

Gradle

implementation("io.ktor:ktor-client-jackson:2.3.13")

Maven

<dependency>
    <groupId>io.ktor</groupId>
    <artifactId>ktor-client-jackson-jvm</artifactId>
    <version>2.3.13</version>
</dependency>

Usage Examples

Basic Usage

import io.ktor.client.*
import io.ktor.client.plugins.json.*
import io.ktor.client.plugins.jackson.*

val client = HttpClient {
    install(JsonPlugin) {
        serializer = JacksonSerializer()
    }
}

Custom ObjectMapper Configuration

val client = HttpClient {
    install(JsonPlugin) {
        serializer = JacksonSerializer {
            // Configure ObjectMapper
            configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
            configure(SerializationFeature.INDENT_OUTPUT, true)
            configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
            
            // Property naming strategy
            propertyNamingStrategy = PropertyNamingStrategies.SNAKE_CASE
            
            // Date format
            setDateFormat(SimpleDateFormat("yyyy-MM-dd HH:mm:ss"))
            
            // Null value handling
            setSerializationInclusion(JsonInclude.Include.NON_NULL)
            
            // Custom modules
            registerModule(JavaTimeModule())
            registerModule(KotlinModule.Builder().build())
        }
    }
}

Data Classes with Jackson

import com.fasterxml.jackson.annotation.*
import java.time.LocalDateTime

data class User(
    val id: Long,
    val name: String,
    val email: String,
    @JsonProperty("created_at")
    val createdAt: LocalDateTime,
    @JsonIgnore
    val password: String? = null
)

// POST request - automatic serialization
val response = client.post("https://api.example.com/users") {
    contentType(ContentType.Application.Json)
    setBody(User(0, "Alice", "alice@example.com", LocalDateTime.now()))
}

// GET request - automatic deserialization
val user: User = client.get("https://api.example.com/users/1").body()

Custom Serializers and Deserializers

import com.fasterxml.jackson.core.*
import com.fasterxml.jackson.databind.*
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

class LocalDateTimeSerializer : JsonSerializer<LocalDateTime>() {
    private val formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME
    
    override fun serialize(
        value: LocalDateTime,
        gen: JsonGenerator,
        serializers: SerializerProvider
    ) {
        gen.writeString(value.format(formatter))
    }
}

class LocalDateTimeDeserializer : JsonDeserializer<LocalDateTime>() {
    private val formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME
    
    override fun deserialize(
        parser: JsonParser,
        context: DeserializationContext
    ): LocalDateTime {
        return LocalDateTime.parse(parser.valueAsString, formatter)
    }
}

// Usage with custom serializers
val client = HttpClient {
    install(JsonPlugin) {
        serializer = JacksonSerializer {
            val module = SimpleModule()
            module.addSerializer(LocalDateTime::class.java, LocalDateTimeSerializer())
            module.addDeserializer(LocalDateTime::class.java, LocalDateTimeDeserializer())
            registerModule(module)
        }
    }
}

Polymorphic Serialization

import com.fasterxml.jackson.annotation.*

@JsonTypeInfo(
    use = JsonTypeInfo.Id.NAME,
    include = JsonTypeInfo.As.PROPERTY,
    property = "type"
)
@JsonSubTypes(
    JsonSubTypes.Type(value = Dog::class, name = "dog"),
    JsonSubTypes.Type(value = Cat::class, name = "cat")
)
abstract class Animal(
    open val name: String
)

@JsonTypeName("dog")
data class Dog(
    override val name: String,
    val breed: String
) : Animal(name)

@JsonTypeName("cat")
data class Cat(
    override val name: String,
    val color: String
) : Animal(name)

// Usage
val animals = listOf(
    Dog("Buddy", "Golden Retriever"),
    Cat("Whiskers", "Orange")
)

val response = client.post("https://api.example.com/animals") {
    contentType(ContentType.Application.Json)
    setBody(animals)
}

Collection and Map Handling

// Generic type handling with TypeReference
import com.fasterxml.jackson.core.type.TypeReference

// List deserialization
val userListType = object : TypeReference<List<User>>() {}
val users: List<User> = client.get("https://api.example.com/users").body()

// Map deserialization
val userMapType = object : TypeReference<Map<String, User>>() {}
val userMap: Map<String, User> = client.get("https://api.example.com/users/map").body()

// Complex nested structures
data class ApiResponse<T>(
    val data: T,
    val status: String,
    val message: String?
)

val responseType = object : TypeReference<ApiResponse<List<User>>>() {}
val response: ApiResponse<List<User>> = client.get("https://api.example.com/users").body()

Error Handling

import com.fasterxml.jackson.core.*
import com.fasterxml.jackson.databind.*

try {
    val user: User = client.get("https://api.example.com/users/1").body()
} catch (e: JsonParseException) {
    // Malformed JSON
    logger.error("Invalid JSON response", e)
} catch (e: JsonMappingException) {
    // Mapping error (type mismatch, missing properties)
    logger.error("JSON mapping error", e)
} catch (e: JsonProcessingException) {
    // General processing error
    logger.error("JSON processing error", e)
}

Advanced ObjectMapper Configuration

val client = HttpClient {
    install(JsonPlugin) {
        serializer = JacksonSerializer {
            // Deserialization features
            configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
            configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, false)
            configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true)
            configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true)
            
            // Serialization features
            configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
            configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
            configure(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, false)
            configure(SerializationFeature.INDENT_OUTPUT, true)
            
            // Property inclusion
            setSerializationInclusion(JsonInclude.Include.NON_NULL)
            setSerializationInclusion(JsonInclude.Include.NON_EMPTY)
            
            // Visibility settings
            setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE)
            setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
            
            // Property naming strategy
            propertyNamingStrategy = PropertyNamingStrategies.SNAKE_CASE
            
            // Date and time handling
            registerModule(JavaTimeModule())
            setDateFormat(SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"))
            
            // Kotlin support
            registerModule(KotlinModule.Builder()
                .withReflectionCacheSize(512)
                .configure(KotlinFeature.NullToEmptyCollection, false)
                .configure(KotlinFeature.NullToEmptyMap, false)
                .configure(KotlinFeature.SingletonSupport, true)
                .build())
        }
    }
}

MixIn Annotations

// Using MixIn to add annotations to third-party classes
interface UserMixIn {
    @JsonProperty("user_id")
    fun getId(): Long
    
    @JsonIgnore
    fun getPassword(): String?
}

val client = HttpClient {
    install(JsonPlugin) {
        serializer = JacksonSerializer {
            addMixIn(User::class.java, UserMixIn::class.java)
        }
    }
}

Jackson Configuration Options

The JacksonSerializer supports extensive ObjectMapper configuration:

Deserialization Features

  • FAIL_ON_UNKNOWN_PROPERTIES: Control unknown property handling
  • FAIL_ON_NULL_FOR_PRIMITIVES: Handle null values for primitives
  • ACCEPT_EMPTY_STRING_AS_NULL_OBJECT: Empty string handling
  • READ_UNKNOWN_ENUM_VALUES_AS_NULL: Enum value handling

Serialization Features

  • FAIL_ON_EMPTY_BEANS: Empty object handling
  • WRITE_DATES_AS_TIMESTAMPS: Date format control
  • INDENT_OUTPUT: Pretty printing
  • WRITE_NULL_MAP_VALUES: Null value inclusion

Property Inclusion

  • JsonInclude.Include.NON_NULL: Exclude null values
  • JsonInclude.Include.NON_EMPTY: Exclude empty collections
  • JsonInclude.Include.NON_DEFAULT: Exclude default values

Advanced Features

  • Custom Modules: JavaTimeModule, KotlinModule, etc.
  • Property Naming: Snake case, camel case strategies
  • Type Information: Polymorphic type handling
  • MixIn Annotations: Add annotations to third-party classes
  • Custom Serializers: Fine-grained serialization control

Performance Considerations

  • ObjectMapper instances are thread-safe and should be reused
  • Module registration is expensive - do it during configuration, not per request
  • Consider using @JsonIgnoreProperties(ignoreUnknown = true) for resilient deserialization
  • Custom serializers should be stateless and lightweight
  • Use TypeReference for complex generic types to avoid type erasure issues

Install with Tessl CLI

npx tessl i tessl/maven-io-ktor--ktor-client-json-jvm

docs

gson-serializer.md

index.md

jackson-serializer.md

json-plugin.md

json-serializer.md

kotlinx-serializer.md

tile.json