Kotlin multiplatform JSON serialization library with type-safe, reflectionless approach supporting all platforms
—
Platform-specific extensions providing additional functionality for JVM streams and JavaScript dynamic objects.
JVM-specific extensions for working with InputStream and OutputStream for efficient streaming operations.
/**
* Serializes the value with serializer into a stream using JSON format and UTF-8 encoding
* @param serializer Strategy for serializing type T
* @param value Value to serialize
* @param stream OutputStream to write JSON to
* @throws SerializationException if value cannot be serialized to JSON
* @throws IOException if I/O error occurs during writing
*/
fun <T> Json.encodeToStream(
serializer: SerializationStrategy<T>,
value: T,
stream: OutputStream
)
/**
* Serializes value to stream using serializer retrieved from reified type parameter
* @param value Value to serialize
* @param stream OutputStream to write JSON to
* @throws SerializationException if value cannot be serialized to JSON
* @throws IOException if I/O error occurs during writing
*/
inline fun <reified T> Json.encodeToStream(value: T, stream: OutputStream)
/**
* Deserializes JSON from stream using UTF-8 encoding to value of type T
* Expects exactly one object in the stream
* @param deserializer Strategy for deserializing type T
* @param stream InputStream to read JSON from
* @return Deserialized value of type T
* @throws SerializationException if JSON input cannot be deserialized
* @throws IllegalArgumentException if decoded input is invalid for type T
* @throws IOException if I/O error occurs during reading
*/
fun <T> Json.decodeFromStream(
deserializer: DeserializationStrategy<T>,
stream: InputStream
): T
/**
* Deserializes from stream using deserializer retrieved from reified type parameter
* @param stream InputStream to read JSON from
* @return Deserialized value of type T
* @throws SerializationException if JSON input cannot be deserialized
* @throws IllegalArgumentException if decoded input is invalid for type T
* @throws IOException if I/O error occurs during reading
*/
inline fun <reified T> Json.decodeFromStream(stream: InputStream): T
/**
* Transforms stream into lazily deserialized sequence of elements of type T
* Stream can contain multiple elements separated as format declares
* @param stream InputStream to read JSON sequence from
* @param deserializer Strategy for deserializing type T
* @param format How elements are separated in the stream
* @return Sequence of deserialized elements (evaluated lazily)
* @throws SerializationException if JSON input cannot be deserialized
* @throws IllegalArgumentException if decoded input is invalid for type T
* @throws IOException if I/O error occurs during reading
*/
fun <T> Json.decodeToSequence(
stream: InputStream,
deserializer: DeserializationStrategy<T>,
format: DecodeSequenceMode = DecodeSequenceMode.AUTO_DETECT
): Sequence<T>
/**
* Reified version of decodeToSequence
* @param stream InputStream to read JSON sequence from
* @param format How elements are separated in the stream
* @return Sequence of deserialized elements (evaluated lazily)
*/
inline fun <reified T> Json.decodeToSequence(
stream: InputStream,
format: DecodeSequenceMode = DecodeSequenceMode.AUTO_DETECT
): Sequence<T>Usage Examples:
@Serializable
data class LogEntry(
val timestamp: Long,
val level: String,
val message: String,
val metadata: Map<String, String> = emptyMap()
)
// Encoding to stream
val logEntry = LogEntry(
timestamp = System.currentTimeMillis(),
level = "INFO",
message = "Application started",
metadata = mapOf("version" to "1.0.0", "env" to "production")
)
// Write to file
val outputFile = File("application.log")
outputFile.outputStream().use { stream ->
Json.encodeToStream(logEntry, stream)
}
// Write to ByteArrayOutputStream
val byteArrayStream = ByteArrayOutputStream()
Json.encodeToStream(LogEntry.serializer(), logEntry, byteArrayStream)
val jsonBytes = byteArrayStream.toByteArray()
// Decoding from stream
val inputFile = File("application.log")
val decodedEntry = inputFile.inputStream().use { stream ->
Json.decodeFromStream<LogEntry>(stream)
}
// Decoding with explicit deserializer
val anotherEntry = inputFile.inputStream().use { stream ->
Json.decodeFromStream(LogEntry.serializer(), stream)
}
// Stream processing with sequence
@Serializable
data class SensorReading(
val sensorId: String,
val value: Double,
val timestamp: Long
)
// Multiple readings in whitespace-separated format
val readingsData = """
{"sensorId":"temp01","value":23.5,"timestamp":1634567890123}
{"sensorId":"temp01","value":24.1,"timestamp":1634567890223}
{"sensorId":"temp01","value":23.8,"timestamp":1634567890323}
{"sensorId":"temp02","value":22.3,"timestamp":1634567890423}
"""
val readingsStream = ByteArrayInputStream(readingsData.toByteArray())
val readings = Json.decodeToSequence<SensorReading>(
readingsStream,
DecodeSequenceMode.WHITESPACE_SEPARATED
)
// Process readings lazily
readings.forEach { reading ->
println("Sensor ${reading.sensorId}: ${reading.value}°C at ${reading.timestamp}")
}
// Array-wrapped sequence
val arrayWrappedData = """
[
{"sensorId":"temp01","value":25.0,"timestamp":1634567890523},
{"sensorId":"temp02","value":23.5,"timestamp":1634567890623},
{"sensorId":"temp03","value":24.2,"timestamp":1634567890723}
]
"""
val arrayStream = ByteArrayInputStream(arrayWrappedData.toByteArray())
val arrayReadings = Json.decodeToSequence<SensorReading>(
arrayStream,
DecodeSequenceMode.ARRAY_WRAPPED
)
// Filter and transform during processing
val highTempReadings = arrayReadings
.filter { it.value > 24.0 }
.map { "${it.sensorId}: ${it.value}°C" }
.toList()
// Large file processing
val largeLogFile = File("large-log.jsonl") // JSON Lines format
val errorEntries = largeLogFile.inputStream().use { stream ->
Json.decodeToSequence<LogEntry>(stream, DecodeSequenceMode.WHITESPACE_SEPARATED)
.filter { it.level == "ERROR" }
.take(100) // Only process first 100 errors
.toList()
}JavaScript-specific extensions for working with native JavaScript objects using the dynamic type.
/**
* Converts native JavaScript objects into Kotlin ones, verifying their types
* Equivalent to Json.decodeFromString(JSON.stringify(nativeObj))
* @param deserializer Strategy for deserializing type T
* @param dynamic Native JavaScript object to convert
* @return Kotlin object of type T
* @throws SerializationException if dynamic object cannot be converted to T
*/
fun <T> Json.decodeFromDynamic(deserializer: DeserializationStrategy<T>, dynamic: dynamic): T
/**
* Reified version of decodeFromDynamic
* @param dynamic Native JavaScript object to convert
* @return Kotlin object of type T
*/
inline fun <reified T> Json.decodeFromDynamic(dynamic: dynamic): T
/**
* Converts Kotlin data structures to plain JavaScript objects
* @param serializer Strategy for serializing type T
* @param value Kotlin object to convert
* @return Native JavaScript object
* @throws SerializationException if value cannot be converted to dynamic
*/
fun <T> Json.encodeToDynamic(serializer: SerializationStrategy<T>, value: T): dynamic
/**
* Reified version of encodeToDynamic
* @param value Kotlin object to convert
* @return Native JavaScript object
*/
inline fun <reified T> Json.encodeToDynamic(value: T): dynamicUsage Examples:
@Serializable
data class UserProfile(
val id: String,
val name: String,
val email: String,
val preferences: UserPreferences
)
@Serializable
data class UserPreferences(
val theme: String,
val notifications: Boolean,
val language: String
)
// Working with JavaScript objects from APIs
val jsApiResponse: dynamic = window.fetch("/api/user/profile")
.then { response -> response.json() }
.await()
// Convert JavaScript object to Kotlin
val userProfile = Json.decodeFromDynamic<UserProfile>(jsApiResponse)
// With explicit deserializer
val explicitProfile = Json.decodeFromDynamic(UserProfile.serializer(), jsApiResponse)
// Convert Kotlin object to JavaScript
val preferences = UserPreferences(
theme = "dark",
notifications = true,
language = "en"
)
val jsPreferences = Json.encodeToDynamic(preferences)
// jsPreferences is now a native JavaScript object
// Can be used directly with JavaScript APIs
window.localStorage.setItem("preferences", JSON.stringify(jsPreferences))
// Working with mixed JavaScript/Kotlin code
@Serializable
data class ApiRequest(
val method: String,
val endpoint: String,
val data: JsonElement?
)
// Create request in Kotlin
val apiRequest = ApiRequest(
method = "POST",
endpoint = "/api/users",
data = buildJsonObject {
put("name", "Alice")
put("email", "alice@example.com")
}
)
// Convert to JavaScript object for fetch API
val jsRequest = Json.encodeToDynamic(apiRequest)
// Use with native fetch
val fetchOptions = js("{}")
fetchOptions.method = jsRequest.method
fetchOptions.body = JSON.stringify(jsRequest.data)
fetchOptions.headers = js("{}")
fetchOptions.headers["Content-Type"] = "application/json"
val response = window.fetch(jsRequest.endpoint, fetchOptions).await()
// Process JavaScript response
val jsResponseData: dynamic = response.json().await()
@Serializable
data class ApiResponse(
val success: Boolean,
val data: JsonElement?,
val message: String?
)
val apiResponse = Json.decodeFromDynamic<ApiResponse>(jsResponseData)
// Interop with existing JavaScript libraries
// Assuming a JavaScript chart library expects configuration object
@Serializable
data class ChartConfig(
val type: String,
val data: ChartData,
val options: ChartOptions
)
@Serializable
data class ChartData(
val labels: List<String>,
val datasets: List<Dataset>
)
@Serializable
data class Dataset(
val label: String,
val data: List<Double>,
val backgroundColor: String
)
@Serializable
data class ChartOptions(
val responsive: Boolean,
val maintainAspectRatio: Boolean
)
val chartConfig = ChartConfig(
type = "bar",
data = ChartData(
labels = listOf("Jan", "Feb", "Mar", "Apr"),
datasets = listOf(
Dataset(
label = "Sales",
data = listOf(100.0, 150.0, 200.0, 175.0),
backgroundColor = "#36A2EB"
)
)
),
options = ChartOptions(
responsive = true,
maintainAspectRatio = false
)
)
// Convert to JavaScript object for chart library
val jsChartConfig = Json.encodeToDynamic(chartConfig)
// Use with Chart.js (example)
val canvas = document.getElementById("myChart") as HTMLCanvasElement
val chart = Chart(canvas, jsChartConfig)
// Working with complex nested JavaScript objects
val complexJsObject: dynamic = js("""
{
user: {
profile: {
id: "user123",
name: "Alice",
email: "alice@example.com",
preferences: {
theme: "dark",
notifications: true,
language: "en"
}
},
permissions: ["read", "write"],
metadata: {
lastLogin: 1634567890123,
loginCount: 45
}
}
}
""")
// Extract specific parts
val userProfileJs = complexJsObject.user.profile
val kotlinProfile = Json.decodeFromDynamic<UserProfile>(userProfileJs)
// Handle arrays from JavaScript
val permissionsJs = complexJsObject.user.permissions
val permissions = Json.decodeFromDynamic<List<String>>(permissionsJs)
// Convert metadata map
val metadataJs = complexJsObject.user.metadata
val metadata = Json.decodeFromDynamic<Map<String, Long>>(metadataJs)Important considerations when using platform extensions.
Usage Examples:
// JVM: Resource management
class LogProcessor {
fun processLogFile(filePath: String): List<LogEntry> {
return File(filePath).inputStream().use { stream ->
// Stream is automatically closed even if exception occurs
Json.decodeToSequence<LogEntry>(stream, DecodeSequenceMode.WHITESPACE_SEPARATED)
.filter { it.level in setOf("ERROR", "WARN") }
.toList()
}
}
fun exportLogs(entries: List<LogEntry>, outputPath: String) {
File(outputPath).outputStream().use { stream ->
// Write each entry as separate JSON object
entries.forEach { entry ->
Json.encodeToStream(entry, stream)
stream.write("\n".toByteArray()) // Add newline
}
}
}
}
// JavaScript: Type safety with dynamic objects
fun processApiResponse(jsResponse: dynamic): UserProfile? {
return try {
// Validate structure before converting
if (jsResponse.id == undefined || jsResponse.name == undefined) {
return null
}
Json.decodeFromDynamic<UserProfile>(jsResponse)
} catch (e: SerializationException) {
console.error("Failed to parse user profile", e)
null
}
}
// JavaScript: Working with promises and async operations
suspend fun fetchUserData(userId: String): UserProfile? {
val response = window.fetch("/api/users/$userId").await()
if (!response.ok) {
return null
}
val jsData: dynamic = response.json().await()
return Json.decodeFromDynamic<UserProfile>(jsData)
}
// JavaScript: Integration with localStorage
fun saveUserPreferences(prefs: UserPreferences) {
val jsPrefs = Json.encodeToDynamic(prefs)
window.localStorage.setItem("userPreferences", JSON.stringify(jsPrefs))
}
fun loadUserPreferences(): UserPreferences? {
val stored = window.localStorage.getItem("userPreferences") ?: return null
val jsPrefs = JSON.parse(stored)
return try {
Json.decodeFromDynamic<UserPreferences>(jsPrefs)
} catch (e: Exception) {
null
}
}
// Multiplatform considerations
expect class PlatformSerializer() {
fun serializeForPlatform(data: Any): String
fun deserializeFromPlatform(json: String): Any?
}
// JVM implementation
actual class PlatformSerializer {
actual fun serializeForPlatform(data: Any): String {
// Use stream-based serialization for large objects
val stream = ByteArrayOutputStream()
when (data) {
is Serializable -> {
Json.encodeToStream(data, stream)
return stream.toString(Charsets.UTF_8)
}
else -> return Json.encodeToString(data.toString())
}
}
actual fun deserializeFromPlatform(json: String): Any? {
return try {
val stream = ByteArrayInputStream(json.toByteArray())
Json.decodeFromStream<JsonElement>(stream)
} catch (e: Exception) {
null
}
}
}
// JavaScript implementation
actual class PlatformSerializer {
actual fun serializeForPlatform(data: Any): String {
// Use dynamic serialization for JavaScript interop
val dynamic = Json.encodeToDynamic(data)
return JSON.stringify(dynamic)
}
actual fun deserializeFromPlatform(json: String): Any? {
return try {
val jsObject = JSON.parse(json)
Json.decodeFromDynamic<JsonElement>(jsObject)
} catch (e: Exception) {
null
}
}
}Install with Tessl CLI
npx tessl i tessl/maven-org-jetbrains-kotlinx--kotlinx-serialization-json