From c855862e7c376686d63ca7a56e601a666d879b1e Mon Sep 17 00:00:00 2001 From: Natan Date: Sat, 20 Dec 2025 17:03:57 -0300 Subject: [PATCH 1/5] ContainerConfig `command` field can be null --- .../me/devnatan/dockerkt/models/container/ContainerConfig.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commonMain/kotlin/me/devnatan/dockerkt/models/container/ContainerConfig.kt b/src/commonMain/kotlin/me/devnatan/dockerkt/models/container/ContainerConfig.kt index 663807b0..a5c14bfa 100644 --- a/src/commonMain/kotlin/me/devnatan/dockerkt/models/container/ContainerConfig.kt +++ b/src/commonMain/kotlin/me/devnatan/dockerkt/models/container/ContainerConfig.kt @@ -24,7 +24,7 @@ public data class ContainerConfig( @SerialName("OpenStdin") public val openStdin: Boolean? = null, @SerialName("StdinOnce") public val stdinOnce: Boolean? = null, @SerialName("Env") public val env: List? = emptyList(), - @SerialName("Cmd") public val command: List = emptyList(), + @SerialName("Cmd") public val command: List? = emptyList(), @SerialName("Healthcheck") public val healthcheck: HealthConfig? = null, @SerialName("ArgsEscaped") public val argsEscaped: Boolean? = null, @SerialName("Image") public val image: String? = null, From 9e57a97d873b36834cd81525b2979316c3229c13 Mon Sep 17 00:00:00 2001 From: Natan Date: Sat, 20 Dec 2025 17:12:07 -0300 Subject: [PATCH 2/5] Do not require explicit nulls in JSON deserialization --- src/commonMain/kotlin/me/devnatan/dockerkt/util/Json.kt | 9 ++++----- .../kotlin/me/devnatan/dockerkt/DockerClient.jvm.kt | 3 ++- .../kotlin/me/devnatan/dockerkt/DockerClient.native.kt | 3 ++- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/commonMain/kotlin/me/devnatan/dockerkt/util/Json.kt b/src/commonMain/kotlin/me/devnatan/dockerkt/util/Json.kt index fd222a1c..a8e92fd8 100644 --- a/src/commonMain/kotlin/me/devnatan/dockerkt/util/Json.kt +++ b/src/commonMain/kotlin/me/devnatan/dockerkt/util/Json.kt @@ -1,15 +1,14 @@ package me.devnatan.dockerkt.util -import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -private val json: Json = +public val DockerKotlinJson: Json = Json { ignoreUnknownKeys = true - isLenient = true allowStructuredMapKeys = true + explicitNulls = false } -public fun toJsonEncodedString(value: Any): String = json.encodeToString(value) +public fun toJsonEncodedString(value: Any): String = DockerKotlinJson.encodeToString(value) -public fun fromJsonEncodedString(value: String): Map = json.decodeFromString(value) +public fun fromJsonEncodedString(value: String): Map = DockerKotlinJson.decodeFromString(value) diff --git a/src/jvmMain/kotlin/me/devnatan/dockerkt/DockerClient.jvm.kt b/src/jvmMain/kotlin/me/devnatan/dockerkt/DockerClient.jvm.kt index 42619406..ae68379c 100644 --- a/src/jvmMain/kotlin/me/devnatan/dockerkt/DockerClient.jvm.kt +++ b/src/jvmMain/kotlin/me/devnatan/dockerkt/DockerClient.jvm.kt @@ -13,6 +13,7 @@ import me.devnatan.dockerkt.resource.network.NetworkResource import me.devnatan.dockerkt.resource.secret.SecretResource import me.devnatan.dockerkt.resource.system.SystemResource import me.devnatan.dockerkt.resource.volume.VolumeResource +import me.devnatan.dockerkt.util.DockerKotlinJson import kotlin.coroutines.CoroutineContext public actual class DockerClient public actual constructor( @@ -22,7 +23,7 @@ public actual class DockerClient public actual constructor( actual override val coroutineContext: CoroutineContext = SupervisorJob() - public actual val json: Json = Json { ignoreUnknownKeys = true } + public actual val json: Json get() = DockerKotlinJson public actual val httpClient: HttpClient = createHttpClient(this) @get:JvmName("images") diff --git a/src/nativeMain/kotlin/me/devnatan/dockerkt/DockerClient.native.kt b/src/nativeMain/kotlin/me/devnatan/dockerkt/DockerClient.native.kt index 3fbaecc2..335ec4ab 100644 --- a/src/nativeMain/kotlin/me/devnatan/dockerkt/DockerClient.native.kt +++ b/src/nativeMain/kotlin/me/devnatan/dockerkt/DockerClient.native.kt @@ -13,6 +13,7 @@ import me.devnatan.dockerkt.resource.network.NetworkResource import me.devnatan.dockerkt.resource.secret.SecretResource import me.devnatan.dockerkt.resource.system.SystemResource import me.devnatan.dockerkt.resource.volume.VolumeResource +import me.devnatan.dockerkt.util.DockerKotlinJson import kotlin.coroutines.CoroutineContext public actual class DockerClient public actual constructor( @@ -21,7 +22,7 @@ public actual class DockerClient public actual constructor( Closeable { actual override val coroutineContext: CoroutineContext = SupervisorJob() - public actual val json: Json = Json { ignoreUnknownKeys = true } + public actual val json: Json get() = DockerKotlinJson public actual val httpClient: HttpClient = createHttpClient(this) public actual val images: ImageResource = ImageResource(httpClient, json) From 35a4e3fb61c0923b465adfbba81fa13e36400488 Mon Sep 17 00:00:00 2001 From: Natan Date: Sat, 20 Dec 2025 17:39:42 -0300 Subject: [PATCH 3/5] Enable `coerceInputValues` --- src/commonMain/kotlin/me/devnatan/dockerkt/util/Json.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commonMain/kotlin/me/devnatan/dockerkt/util/Json.kt b/src/commonMain/kotlin/me/devnatan/dockerkt/util/Json.kt index a8e92fd8..2f5699c2 100644 --- a/src/commonMain/kotlin/me/devnatan/dockerkt/util/Json.kt +++ b/src/commonMain/kotlin/me/devnatan/dockerkt/util/Json.kt @@ -6,7 +6,7 @@ public val DockerKotlinJson: Json = Json { ignoreUnknownKeys = true allowStructuredMapKeys = true - explicitNulls = false + coerceInputValues = true } public fun toJsonEncodedString(value: Any): String = DockerKotlinJson.encodeToString(value) From fec0daa0dfa497da55a141e41db0518b15ef0481 Mon Sep 17 00:00:00 2001 From: Natan Date: Sat, 20 Dec 2025 17:39:58 -0300 Subject: [PATCH 4/5] Write testes to ensure JSON deserialization follows rules --- .../models/container/ContainerConfig.kt | 2 +- .../util/ListAsMapToEmptyObjectsSerializer.kt | 4 ++-- .../me/devnatan/dockerkt/util/JsonTest.kt | 24 +++++++++++++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 src/commonTest/kotlin/me/devnatan/dockerkt/util/JsonTest.kt diff --git a/src/commonMain/kotlin/me/devnatan/dockerkt/models/container/ContainerConfig.kt b/src/commonMain/kotlin/me/devnatan/dockerkt/models/container/ContainerConfig.kt index a5c14bfa..663807b0 100644 --- a/src/commonMain/kotlin/me/devnatan/dockerkt/models/container/ContainerConfig.kt +++ b/src/commonMain/kotlin/me/devnatan/dockerkt/models/container/ContainerConfig.kt @@ -24,7 +24,7 @@ public data class ContainerConfig( @SerialName("OpenStdin") public val openStdin: Boolean? = null, @SerialName("StdinOnce") public val stdinOnce: Boolean? = null, @SerialName("Env") public val env: List? = emptyList(), - @SerialName("Cmd") public val command: List? = emptyList(), + @SerialName("Cmd") public val command: List = emptyList(), @SerialName("Healthcheck") public val healthcheck: HealthConfig? = null, @SerialName("ArgsEscaped") public val argsEscaped: Boolean? = null, @SerialName("Image") public val image: String? = null, diff --git a/src/commonMain/kotlin/me/devnatan/dockerkt/util/ListAsMapToEmptyObjectsSerializer.kt b/src/commonMain/kotlin/me/devnatan/dockerkt/util/ListAsMapToEmptyObjectsSerializer.kt index 515d59e2..54801b66 100644 --- a/src/commonMain/kotlin/me/devnatan/dockerkt/util/ListAsMapToEmptyObjectsSerializer.kt +++ b/src/commonMain/kotlin/me/devnatan/dockerkt/util/ListAsMapToEmptyObjectsSerializer.kt @@ -12,8 +12,8 @@ import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive internal open class ListAsMapToEmptyObjectsSerializer( - private val tSerializer: KSerializer, -) : JsonTransformingSerializer>(ListSerializer(tSerializer)) { + serializer: KSerializer, +) : JsonTransformingSerializer>(ListSerializer(serializer)) { override fun transformDeserialize(element: JsonElement): JsonElement = JsonArray(element.jsonObject.entries.map { JsonPrimitive(it.key) }) diff --git a/src/commonTest/kotlin/me/devnatan/dockerkt/util/JsonTest.kt b/src/commonTest/kotlin/me/devnatan/dockerkt/util/JsonTest.kt new file mode 100644 index 00000000..a90a1e01 --- /dev/null +++ b/src/commonTest/kotlin/me/devnatan/dockerkt/util/JsonTest.kt @@ -0,0 +1,24 @@ +package me.devnatan.dockerkt.util + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlin.test.Test +import kotlin.test.assertEquals + +class JsonTest { + @Test + fun `deserialization of nullable api field with default value`() { + @Serializable + data class Entity( + @SerialName("Cmd") val command: List = emptyList(), + ) + + val json = """{"Cmd": null}""" + val entity = DockerKotlinJson.decodeFromString(json) + + assertEquals( + expected = Entity(command = emptyList()), + actual = entity, + ) + } +} From 151a40586adf4b454e796e8ef7b634f7dce40c81 Mon Sep 17 00:00:00 2001 From: Natan Date: Sat, 20 Dec 2025 17:42:23 -0300 Subject: [PATCH 5/5] Update ABI --- api/docker-kotlin.api | 3 ++- api/docker-kotlin.klib.api | 2 ++ src/commonMain/kotlin/me/devnatan/dockerkt/util/Json.kt | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/api/docker-kotlin.api b/api/docker-kotlin.api index 88401015..f988c7e2 100644 --- a/api/docker-kotlin.api +++ b/api/docker-kotlin.api @@ -4020,8 +4020,9 @@ public final class me/devnatan/dockerkt/resource/volume/VolumeResourceKt { public static final fun remove (Lme/devnatan/dockerkt/resource/volume/VolumeResource;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } -public final class me/devnatan/dockerkt/util/JsonKt { +public final synthetic class me/devnatan/dockerkt/util/JsonKt { public static final fun fromJsonEncodedString (Ljava/lang/String;)Ljava/util/Map; + public static final fun getDockerKotlinJson ()Lkotlinx/serialization/json/Json; public static final fun toJsonEncodedString (Ljava/lang/Object;)Ljava/lang/String; } diff --git a/api/docker-kotlin.klib.api b/api/docker-kotlin.klib.api index a8bfaf12..0d48e7be 100644 --- a/api/docker-kotlin.klib.api +++ b/api/docker-kotlin.klib.api @@ -4150,6 +4150,8 @@ final val me.devnatan.dockerkt.models.image/created // me.devnatan.dockerkt.mode final fun (me.devnatan.dockerkt.models.image/Image).(): kotlin.time/Instant // me.devnatan.dockerkt.models.image/created.|@me.devnatan.dockerkt.models.image.Image(){}[0] final val me.devnatan.dockerkt.models.system/time // me.devnatan.dockerkt.models.system/time|@me.devnatan.dockerkt.models.system.Event{}time[0] final fun (me.devnatan.dockerkt.models.system/Event).(): kotlin.time/Instant // me.devnatan.dockerkt.models.system/time.|@me.devnatan.dockerkt.models.system.Event(){}[0] +final val me.devnatan.dockerkt.util/DockerKotlinJson // me.devnatan.dockerkt.util/DockerKotlinJson|{}DockerKotlinJson[0] + final fun (): kotlinx.serialization.json/Json // me.devnatan.dockerkt.util/DockerKotlinJson.|(){}[0] final var me.devnatan.dockerkt.models.container/stopTimeout // me.devnatan.dockerkt.models.container/stopTimeout|@me.devnatan.dockerkt.models.container.ContainerCreateOptions{}stopTimeout[0] final fun (me.devnatan.dockerkt.models.container/ContainerCreateOptions).(): kotlin.time/Duration? // me.devnatan.dockerkt.models.container/stopTimeout.|@me.devnatan.dockerkt.models.container.ContainerCreateOptions(){}[0] diff --git a/src/commonMain/kotlin/me/devnatan/dockerkt/util/Json.kt b/src/commonMain/kotlin/me/devnatan/dockerkt/util/Json.kt index 2f5699c2..c7690634 100644 --- a/src/commonMain/kotlin/me/devnatan/dockerkt/util/Json.kt +++ b/src/commonMain/kotlin/me/devnatan/dockerkt/util/Json.kt @@ -1,6 +1,9 @@ +@file:JvmSynthetic + package me.devnatan.dockerkt.util import kotlinx.serialization.json.Json +import kotlin.jvm.JvmSynthetic public val DockerKotlinJson: Json = Json {