From e7d3bcdc01563062baedd5e39c4743fb4e9cb038 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 19 Sep 2026 13:53:09 +0000 Subject: [PATCH] perf: drop jackson-module-kotlin, removing kotlin-reflect from the app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nothing in the app binds JSON reflectively any more, so the module whose whole job is reflective Kotlin binding has no work left to do. Swapping it for plain jackson-databind takes kotlin-reflect out with it: kotlin.reflect.jvm.internal 969 -> 0 classes com.fasterxml.jackson.module.kotlin 73 -> 0 classes DEX 30.94 MB -> 29.94 MB, 3 files -> 2 A megabyte and a whole secondary DEX, for code the app never called. `jacksonObjectMapper()` became `ObjectMapper()` in all eight remaining places: JacksonMapper and JsonMapperNip55 (both drive registered StdSerializer / StdDeserializer pairs) and six readTree-only users (LNURL resolvers, TorService, ArtiGuardState, Nip96Uploader). None of them needed the Kotlin module. The one thing the module did that databind has no equivalent for is `jacksonTypeRef()`, which NIP-55 needs: `readValue>` erases to `List` under `T::class.java` and every element comes back a LinkedHashMap. It is a one-liner — `reified` substitutes the concrete type into the anonymous subclass before erasure — so it now lives in quartz as jacksonTypeRefOf(). quartz exported the module as an `api` dependency, so test sources three modules away were quietly using it. Those move over too, same two swaps: `ObjectMapper()` where the mapper only walks maps or readTree (the pretty-printer test, the BIP-39 vectors, the event-command benchmark, quic's qlog writer and its test), and `jacksonTypeRefOf>()` where the reified `readValue` extension was doing the typeref's job (LargeDBSignatureCheck, the thread-order test, the two cache benchmarks). Event's own deserializer is registered on the mapper, so databind resolves it either way. `cli` keeps the module. It genuinely binds its config files reflectively (Config, OperatorKeys, Aliases, StatusReport), it declares the dependency itself, and it is never minified, so none of this applies there. desktopApp, geode, relayBench and quic-interop likewise declare it directly and are unaffected. Verified: `./gradlew test` green, the instrumented sources compile (:quartz:compileAndroidDeviceTestSources, :amethyst and :benchmark androidTest), R8 release build green, 19/19 contract checks, and both package prefixes confirmed at zero classes in the shipped DEX. Note for whoever hits it next: a full recompile of :amethyst needs more than a 2 GB Kotlin daemon. Mine died with "Couldn't transform method node" pointing at a Compose lambda in NoteCompose.kt, which is an OutOfMemoryError in the ASM frame analyzer wearing a disguise, not a code fault. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01DEoxktEZyTrAS33vVZBiwm --- .../ThreadDualAxisChartAssemblerTest.kt | 4 +- .../service/lnurl/LightningAddressResolver.kt | 6 +-- .../service/uploads/nip96/Nip96Uploader.kt | 4 +- .../amethyst/ui/tor/ArtiGuardState.kt | 4 +- .../amethyst/ui/tor/TorService.kt | 4 +- .../benchmark/BaseLargeCacheBenchmark.kt | 5 ++- .../quartz/benchmark/CacheBenchmark.kt | 5 ++- .../benchmark/EventCmdSerializerBenchmark.kt | 6 +-- .../service/lnurl/LightningAddressResolver.kt | 4 +- .../lnurl/OkHttpLnurlEndpointResolver.kt | 4 +- gradle/libs.versions.toml | 2 + quartz/build.gradle.kts | 12 +++++- .../quartz/LargeDBSignatureCheck.kt | 8 ++-- .../nip06KeyDerivation/Bip39MnemonicsTest.kt | 4 +- .../nip55AndroidSigner/JsonMapperNip55.kt | 9 ++--- .../quartz/nip01Core/jackson/JacksonMapper.kt | 24 ++++++------ .../nip01Core/jackson/JacksonTypeRef.kt | 39 +++++++++++++++++++ .../InliningTagArrayPrettyPrinterTest.kt | 4 +- .../vitorpamplona/quic/interop/QlogWriter.kt | 3 +- .../quic/interop/QlogWriterTest.kt | 8 ++-- 20 files changed, 104 insertions(+), 55 deletions(-) create mode 100644 quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonTypeRef.kt diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt index 39b9db024d..d799adb156 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.amethyst import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.fasterxml.jackson.module.kotlin.readValue import com.vitorpamplona.amethyst.commons.relayClient.assemblers.CashuMintDirectoryFilterAssembler import com.vitorpamplona.amethyst.commons.relayClient.nip47WalletConnect.NWCPaymentFilterAssembler import com.vitorpamplona.amethyst.commons.viewmodels.thread.ThreadFeedFilter @@ -34,6 +33,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.crypto.verify import com.vitorpamplona.quartz.nip01Core.jackson.JacksonMapper +import com.vitorpamplona.quartz.nip01Core.jackson.jacksonTypeRefOf import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag @@ -163,7 +163,7 @@ class ThreadDualAxisChartAssemblerTest { fun threadOrderTest() = runBlocking { val eventArray = - JacksonMapper.mapper.readValue>(db) + Event.fromJson(header) + JacksonMapper.mapper.readValue(db, jacksonTypeRefOf>()) + Event.fromJson(header) var counter = 0 eventArray.forEach { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt index 2ed7dce908..305cb433a2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.service.lnurl import android.content.Context -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.databind.ObjectMapper import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.HttpStatusMessages import com.vitorpamplona.amethyst.ui.stringRes @@ -167,7 +167,7 @@ class LightningAddressResolver { val errorMessage = runCatching { - jacksonObjectMapper().readTree(body) + ObjectMapper().readTree(body) }.getOrNull()?.let { tree -> val errorNode = tree.get("error") val messageNode = tree.get("message") @@ -218,7 +218,7 @@ class LightningAddressResolver { context: Context, onZapRequestSent: (LnZapRequestEvent?) -> Unit = {}, ): String { - val mapper = jacksonObjectMapper() + val mapper = ObjectMapper() val lnurlpUrl = assembleUrl(lnAddress) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt index c52095af2a..da9f6875ca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt @@ -26,7 +26,7 @@ import android.net.Uri import android.provider.OpenableColumns import android.webkit.MimeTypeMap import androidx.core.net.toFile -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.databind.ObjectMapper import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.HttpStatusMessages import com.vitorpamplona.amethyst.service.checkNotInMainThread @@ -209,7 +209,7 @@ class Nip96Uploader { val errorMessage = try { - val tree = jacksonObjectMapper().readTree(msg) + val tree = ObjectMapper().readTree(msg) val status = tree.get("status")?.asText() val message = tree.get("message")?.asText() if (status == "error" && message != null) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/ArtiGuardState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/ArtiGuardState.kt index 5ceab6f135..67ca1ed0b9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/ArtiGuardState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/ArtiGuardState.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.ui.tor import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.databind.ObjectMapper /** * Pure, file- and JNI-free parsers over Arti's persisted guard sample @@ -42,7 +42,7 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper * circuit — i.e. a real bootstrap reached the guard-confirmation stage. */ object ArtiGuardState { - private val mapper = jacksonObjectMapper() + private val mapper = ObjectMapper() /** Convenience for tests/callers holding the raw file text. */ fun parse(json: String): JsonNode = mapper.readTree(json) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorService.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorService.kt index e8566a2275..5384e3f808 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorService.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorService.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.tor import android.content.Context import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.databind.ObjectMapper import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -206,7 +206,7 @@ class TorService( val file = guardsFile() if (!file.exists()) return null return try { - jacksonObjectMapper().readTree(file) + ObjectMapper().readTree(file) } catch (e: Exception) { Log.w("TorService") { "Could not inspect guards.json: ${e.message}" } null diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/BaseLargeCacheBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/BaseLargeCacheBenchmark.kt index 1da2be3a7e..cbd06b89d1 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/BaseLargeCacheBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/BaseLargeCacheBenchmark.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.quartz.benchmark -import com.fasterxml.jackson.module.kotlin.readValue import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.jackson.JacksonMapper +import com.vitorpamplona.quartz.nip01Core.jackson.jacksonTypeRefOf import com.vitorpamplona.quartz.utils.cache.LargeCache import org.junit.Assert.assertTrue import java.util.function.Consumer @@ -35,8 +35,9 @@ open class BaseLargeCacheBenchmark { // This file includes duplicates val fullDBInputStream = javaClass.classLoader!!.getResourceAsStream("nostr_vitor_startup_data.json.gz") - return JacksonMapper.mapper.readValue>( + return JacksonMapper.mapper.readValue( GZIPInputStream(fullDBInputStream), + jacksonTypeRefOf>(), ) } } diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/CacheBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/CacheBenchmark.kt index d9138f5dce..dbec80137d 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/CacheBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/CacheBenchmark.kt @@ -23,10 +23,10 @@ package com.vitorpamplona.quartz.benchmark import androidx.benchmark.junit4.BenchmarkRule import androidx.benchmark.junit4.measureRepeated import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.fasterxml.jackson.module.kotlin.readValue import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.jackson.JacksonMapper +import com.vitorpamplona.quartz.nip01Core.jackson.jacksonTypeRefOf import com.vitorpamplona.quartz.utils.cache.LargeCache import org.junit.Assert.assertTrue import org.junit.Rule @@ -43,8 +43,9 @@ open class BaseCacheBenchmark { // This file includes duplicates val fullDBInputStream = javaClass.classLoader?.getResourceAsStream("nostr_vitor_startup_data.json.gz") - return JacksonMapper.mapper.readValue>( + return JacksonMapper.mapper.readValue( GZIPInputStream(fullDBInputStream), + jacksonTypeRefOf>(), ) } diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventCmdSerializerBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventCmdSerializerBenchmark.kt index 1eec88137d..884e353321 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventCmdSerializerBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventCmdSerializerBenchmark.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.quartz.benchmark import androidx.benchmark.junit4.BenchmarkRule import androidx.benchmark.junit4.measureRepeated import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.databind.ObjectMapper import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.utils.sha256.Sha256Hasher import org.junit.Rule @@ -54,7 +54,7 @@ class EventCmdSerializerBenchmark { @Test fun jsonStringEncoderJackson() { - val jsonMapper = jacksonObjectMapper() + val jsonMapper = ObjectMapper() benchmarkRule.measureRepeated { jsonMapper.writeValueAsString(specialEncoders) } @@ -62,7 +62,7 @@ class EventCmdSerializerBenchmark { @Test fun jsonStringEncoderSha256Jackson() { - val jsonMapper = jacksonObjectMapper() + val jsonMapper = ObjectMapper() benchmarkRule.measureRepeated { val digest = Sha256Hasher() digest.hash(jsonMapper.writeValueAsString(specialEncoders).toByteArray()) diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/lnurl/LightningAddressResolver.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/lnurl/LightningAddressResolver.kt index 9161df6764..5acca96399 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/lnurl/LightningAddressResolver.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/lnurl/LightningAddressResolver.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.commons.service.lnurl -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.databind.ObjectMapper import com.vitorpamplona.quartz.lightning.LnInvoiceUtil import com.vitorpamplona.quartz.lightning.Lud06 import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent @@ -48,7 +48,7 @@ import kotlin.coroutines.cancellation.CancellationException class LightningAddressResolver( private val httpClient: OkHttpClient, ) { - private val mapper = jacksonObjectMapper() + private val mapper = ObjectMapper() /** * Result of resolving a lightning address to a BOLT11 invoice. diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/lnurl/OkHttpLnurlEndpointResolver.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/lnurl/OkHttpLnurlEndpointResolver.kt index 2fbdf41481..dd162f881c 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/lnurl/OkHttpLnurlEndpointResolver.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/lnurl/OkHttpLnurlEndpointResolver.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.commons.service.lnurl -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.databind.ObjectMapper import com.vitorpamplona.quartz.nip57Zaps.validate.LnurlEndpointCache import com.vitorpamplona.quartz.nip57Zaps.validate.LnurlEndpointInfo import com.vitorpamplona.quartz.nip57Zaps.validate.LnurlEndpointResolver @@ -47,7 +47,7 @@ import kotlin.coroutines.cancellation.CancellationException class OkHttpLnurlEndpointResolver( private val okHttpClient: (String) -> OkHttpClient, ) : LnurlEndpointResolver { - private val mapper = jacksonObjectMapper() + private val mapper = ObjectMapper() override suspend fun resolve(lnurlpUrl: String): LnurlEndpointInfo? = LnurlEndpointCache.getOrFetch(lnurlpUrl, ::fetch) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cfaf21993d..8ddf59c670 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -38,6 +38,7 @@ firebaseBom = "34.19.0" fragmentKtx = "1.9.0" gms = "4.5.0" healthConnect = "1.1.0" +jackson = "2.22.2" jacksonModuleKotlin = "2.22.2" javaKeyring = "1.0.4" kmpTorRuntime = "2.6.0" @@ -203,6 +204,7 @@ google-mlkit-genai-rewriting = { group = "com.google.mlkit", name = "genai-rewri google-mlkit-genai-image-description = { group = "com.google.mlkit", name = "genai-image-description", version.ref = "genaiImageDescription" } google-mlkit-language-id = { group = "com.google.mlkit", name = "language-id", version.ref = "languageId" } google-mlkit-translate = { group = "com.google.mlkit", name = "translate", version.ref = "translate" } +jackson-databind = { group = "com.fasterxml.jackson.core", name = "jackson-databind", version.ref = "jackson" } jackson-module-kotlin = { group = "com.fasterxml.jackson.module", name = "jackson-module-kotlin", version.ref = "jacksonModuleKotlin" } java-keyring = { group = "com.github.javakeyring", name = "java-keyring", version.ref = "javaKeyring" } kmp-tor-runtime = { group = "io.matthewnelson.kmp-tor", name = "runtime", version.ref = "kmpTorRuntime" } diff --git a/quartz/build.gradle.kts b/quartz/build.gradle.kts index 9db22ebaf1..2ae70ca1cb 100644 --- a/quartz/build.gradle.kts +++ b/quartz/build.gradle.kts @@ -177,8 +177,16 @@ kotlin { dependsOn(commonMain.get()) dependencies { - // Performant Parser of JSONs into Events - api(libs.jackson.module.kotlin) + // Performant Parser of JSONs into Events. + // + // jackson-databind, NOT jackson-module-kotlin: the module exists to bind + // Kotlin classes reflectively off their constructor parameter names, and + // nothing in the app does that any more — every wire format goes through a + // hand-written serializer or kotlinx. Dropping it takes kotlin-reflect with + // it, which is ~1,000 classes of DEX the app never called. `cli` still + // declares the module itself: it genuinely binds its config files + // reflectively, and is never minified. + api(libs.jackson.databind) // Websockets API implementation(libs.okhttp) diff --git a/quartz/src/androidDeviceTest/kotlin/com/vitorpamplona/quartz/LargeDBSignatureCheck.kt b/quartz/src/androidDeviceTest/kotlin/com/vitorpamplona/quartz/LargeDBSignatureCheck.kt index f7f5b655a0..555276c6d6 100644 --- a/quartz/src/androidDeviceTest/kotlin/com/vitorpamplona/quartz/LargeDBSignatureCheck.kt +++ b/quartz/src/androidDeviceTest/kotlin/com/vitorpamplona/quartz/LargeDBSignatureCheck.kt @@ -21,10 +21,10 @@ package com.vitorpamplona.quartz import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.fasterxml.jackson.module.kotlin.readValue import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.crypto.verify import com.vitorpamplona.quartz.nip01Core.jackson.JacksonMapper +import com.vitorpamplona.quartz.nip01Core.jackson.jacksonTypeRefOf import junit.framework.TestCase.assertEquals import kotlinx.coroutines.runBlocking import org.junit.Assert.assertTrue @@ -41,8 +41,9 @@ class LargeDBSignatureCheck { val fullDBInputStream = javaClass.classLoader?.getResourceAsStream("nostr_vitor_short.json") val eventArray = - JacksonMapper.mapper.readValue>( + JacksonMapper.mapper.readValue( InputStreamReader(fullDBInputStream), + jacksonTypeRefOf>(), ) as List var counter = 0 @@ -61,8 +62,9 @@ class LargeDBSignatureCheck { val fullDBInputStream = javaClass.classLoader?.getResourceAsStream("nostr_vitor_startup_data.json.gz") val eventArray = - JacksonMapper.mapper.readValue>( + JacksonMapper.mapper.readValue( GZIPInputStream(fullDBInputStream), + jacksonTypeRefOf>(), ) as List var counter = 0 diff --git a/quartz/src/androidDeviceTest/kotlin/com/vitorpamplona/quartz/nip06KeyDerivation/Bip39MnemonicsTest.kt b/quartz/src/androidDeviceTest/kotlin/com/vitorpamplona/quartz/nip06KeyDerivation/Bip39MnemonicsTest.kt index 9042339582..0eabd91da5 100644 --- a/quartz/src/androidDeviceTest/kotlin/com/vitorpamplona/quartz/nip06KeyDerivation/Bip39MnemonicsTest.kt +++ b/quartz/src/androidDeviceTest/kotlin/com/vitorpamplona/quartz/nip06KeyDerivation/Bip39MnemonicsTest.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip06KeyDerivation import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.databind.ObjectMapper import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.RandomInstance @@ -34,7 +34,7 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class Bip39MnemonicsTest { private val tests = - jacksonObjectMapper() + ObjectMapper() .readTree(javaClass.classLoader?.getResourceAsStream("bip39.vectors.json")) @Test diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/JsonMapperNip55.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/JsonMapperNip55.kt index 41123a7386..06e5867c6d 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/JsonMapperNip55.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/JsonMapperNip55.kt @@ -26,9 +26,8 @@ import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.databind.node.ArrayNode import com.fasterxml.jackson.databind.node.ObjectNode -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.fasterxml.jackson.module.kotlin.readValue import com.vitorpamplona.quartz.nip01Core.jackson.InliningTagArrayPrettyPrinter +import com.vitorpamplona.quartz.nip01Core.jackson.jacksonTypeRefOf import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.results.IntentResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.results.IntentResultJsonDeserializer import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.results.IntentResultJsonSerializer @@ -39,7 +38,7 @@ import java.io.InputStream object JsonMapperNip55 { val defaultMapper: ObjectMapper = - jacksonObjectMapper() + ObjectMapper() .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature()) .setDefaultPrettyPrinter(InliningTagArrayPrettyPrinter()) @@ -51,9 +50,9 @@ object JsonMapperNip55 { .addSerializer(Permission::class.java, PermissionSerializer()), ) - inline fun fromJsonTo(json: String): T = defaultMapper.readValue(json) + inline fun fromJsonTo(json: String): T = defaultMapper.readValue(json, jacksonTypeRefOf()) - inline fun fromJsonTo(json: InputStream): T = defaultMapper.readValue(json) + inline fun fromJsonTo(json: InputStream): T = defaultMapper.readValue(json, jacksonTypeRefOf()) fun toJson(event: ArrayNode): String = defaultMapper.writeValueAsString(event) diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapper.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapper.kt index e96f9431cd..ce818cf7f3 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapper.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapper.kt @@ -23,12 +23,10 @@ package com.vitorpamplona.quartz.nip01Core.jackson import com.fasterxml.jackson.core.json.JsonReadFeature import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.JavaType +import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.databind.node.ArrayNode import com.fasterxml.jackson.databind.node.ObjectNode -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.fasterxml.jackson.module.kotlin.jacksonTypeRef -import com.fasterxml.jackson.module.kotlin.readValue import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.OptimizedSerializable import com.vitorpamplona.quartz.nip01Core.core.RawJson @@ -64,7 +62,7 @@ class JacksonMapper { val defaultPrettyPrinter = InliningTagArrayPrettyPrinter() val mapper = - jacksonObjectMapper() + ObjectMapper() .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .configure(DeserializationFeature.FAIL_ON_TRAILING_TOKENS, false) .configure(DeserializationFeature.UNWRAP_ROOT_VALUE, false) @@ -104,13 +102,13 @@ class JacksonMapper { /** * Shortcuts */ - val eventTypeInstance: JavaType = mapper.typeFactory.constructType(jacksonTypeRef()) - val tagArrayTypeInstance: JavaType = mapper.typeFactory.constructType(jacksonTypeRef()) - val rumorTypeInstance: JavaType = mapper.typeFactory.constructType(jacksonTypeRef()) - val eventTemplateTypeInstance: JavaType = mapper.typeFactory.constructType(jacksonTypeRef>()) - val eventListTypeInstance: JavaType = mapper.typeFactory.constructType(jacksonTypeRef>()) - val messageTypeInstance: JavaType = mapper.typeFactory.constructType(jacksonTypeRef()) - val commandTypeInstance: JavaType = mapper.typeFactory.constructType(jacksonTypeRef()) + val eventTypeInstance: JavaType = mapper.typeFactory.constructType(jacksonTypeRefOf()) + val tagArrayTypeInstance: JavaType = mapper.typeFactory.constructType(jacksonTypeRefOf()) + val rumorTypeInstance: JavaType = mapper.typeFactory.constructType(jacksonTypeRefOf()) + val eventTemplateTypeInstance: JavaType = mapper.typeFactory.constructType(jacksonTypeRefOf>()) + val eventListTypeInstance: JavaType = mapper.typeFactory.constructType(jacksonTypeRefOf>()) + val messageTypeInstance: JavaType = mapper.typeFactory.constructType(jacksonTypeRefOf()) + val commandTypeInstance: JavaType = mapper.typeFactory.constructType(jacksonTypeRefOf()) fun fromJson(json: String): Event = mapper.readValue(json, eventTypeInstance) @@ -169,12 +167,12 @@ class JacksonMapper { inline fun fromJsonTo(json: String): T { checkRegistered(T::class) - return mapper.readValue(json) + return mapper.readValue(json, jacksonTypeRefOf()) } inline fun fromJsonTo(json: InputStream): T { checkRegistered(T::class) - return mapper.readValue(json) + return mapper.readValue(json, jacksonTypeRefOf()) } fun toJson(event: Event): String = EventManualSerializer.toJson(event.id, event.pubKey, event.createdAt, event.kind, event.tags, event.content, event.sig) diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonTypeRef.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonTypeRef.kt new file mode 100644 index 0000000000..1670f58bce --- /dev/null +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonTypeRef.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.jackson + +import com.fasterxml.jackson.core.type.TypeReference + +/** + * A [TypeReference] carrying the full generic type, e.g. `List`. + * + * This is the one thing the app used jackson-module-kotlin for that plain + * jackson-databind has no equivalent of — and it is a one-liner, because `reified` + * substitutes the concrete type into the anonymous subclass before erasure can + * lose it. `T::class.java` cannot replace it: for `List` that erases to + * `List`, and every element comes back a LinkedHashMap. + * + * Copied here rather than kept as a dependency so the Kotlin module — and + * kotlin-reflect behind it, ~1,000 classes — can leave the app. The CLI still + * depends on the module directly, because it genuinely binds reflectively and is + * never minified. + */ +inline fun jacksonTypeRefOf(): TypeReference = object : TypeReference() {} diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/InliningTagArrayPrettyPrinterTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/InliningTagArrayPrettyPrinterTest.kt index be489b5aae..df327e132d 100644 --- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/InliningTagArrayPrettyPrinterTest.kt +++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/InliningTagArrayPrettyPrinterTest.kt @@ -20,13 +20,13 @@ */ package com.vitorpamplona.quartz.nip01Core.jackson -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.databind.ObjectMapper import kotlin.test.Test import kotlin.test.assertEquals class InliningTagArrayPrettyPrinterTest { val mapper = - jacksonObjectMapper().apply { + ObjectMapper().apply { setDefaultPrettyPrinter(InliningTagArrayPrettyPrinter()) } diff --git a/quic/src/jvmTest/kotlin/com/vitorpamplona/quic/interop/QlogWriter.kt b/quic/src/jvmTest/kotlin/com/vitorpamplona/quic/interop/QlogWriter.kt index 585d17c3e6..eba66bd774 100644 --- a/quic/src/jvmTest/kotlin/com/vitorpamplona/quic/interop/QlogWriter.kt +++ b/quic/src/jvmTest/kotlin/com/vitorpamplona/quic/interop/QlogWriter.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.quic.interop import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.vitorpamplona.quic.connection.EncryptionLevel import com.vitorpamplona.quic.observability.QlogObserver import java.io.BufferedWriter @@ -295,7 +294,7 @@ class QlogWriter( } companion object { - private val DEFAULT_MAPPER: ObjectMapper = jacksonObjectMapper() + private val DEFAULT_MAPPER: ObjectMapper = ObjectMapper() private fun packetTypeFor(level: EncryptionLevel): String = when (level) { diff --git a/quic/src/jvmTest/kotlin/com/vitorpamplona/quic/interop/QlogWriterTest.kt b/quic/src/jvmTest/kotlin/com/vitorpamplona/quic/interop/QlogWriterTest.kt index 72f5d13845..f6e20de0f1 100644 --- a/quic/src/jvmTest/kotlin/com/vitorpamplona/quic/interop/QlogWriterTest.kt +++ b/quic/src/jvmTest/kotlin/com/vitorpamplona/quic/interop/QlogWriterTest.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quic.interop -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.databind.ObjectMapper import com.vitorpamplona.quic.connection.EncryptionLevel import org.junit.Test import java.io.File @@ -73,7 +73,7 @@ class QlogWriterTest { val lines = tmp.readLines().filter { it.isNotBlank() } assertTrue(lines.size >= 12, "expected >= 12 lines (header + at least 11 events) but got ${lines.size}") - val mapper = jacksonObjectMapper() + val mapper = ObjectMapper() // Line 1: qlog header. val header = mapper.readTree(lines[0]) @@ -128,7 +128,7 @@ class QlogWriterTest { w.onAlpnNegotiated("h3") } val lines = tmp.readLines().filter { it.isNotBlank() } - val mapper = jacksonObjectMapper() + val mapper = ObjectMapper() val event = mapper.readTree(lines[1]) assertEquals(50L, event.get("time").asLong(), "time must be relative to constructor (1050 - 1000)") } @@ -152,7 +152,7 @@ class QlogWriterTest { QlogWriter(tmp, odcidHex = "00").use { w -> w.onPacketSent(EncryptionLevel.INITIAL, 0, 1200, emptyList()) } - val mapper = jacksonObjectMapper() + val mapper = ObjectMapper() val lines = tmp.readLines().filter { it.isNotBlank() } val frames = mapper.readTree(lines[1]).get("data").get("frames") assertTrue(frames.isArray, "frames must be an array even when empty")