perf: drop jackson-module-kotlin, removing kotlin-reflect from the app

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<T>()`, which NIP-55 needs: `readValue<List<IntentResult>>` 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<ArrayList<Event>>()` where the reified `readValue<T>` 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DEoxktEZyTrAS33vVZBiwm
This commit is contained in:
Claude
2026-09-19 14:01:07 +00:00
parent 2d1e398910
commit e7d3bcdc01
20 changed files with 104 additions and 55 deletions
@@ -21,7 +21,6 @@
package com.vitorpamplona.amethyst package com.vitorpamplona.amethyst
import androidx.test.ext.junit.runners.AndroidJUnit4 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.assemblers.CashuMintDirectoryFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.nip47WalletConnect.NWCPaymentFilterAssembler import com.vitorpamplona.amethyst.commons.relayClient.nip47WalletConnect.NWCPaymentFilterAssembler
import com.vitorpamplona.amethyst.commons.viewmodels.thread.ThreadFeedFilter 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.KeyPair
import com.vitorpamplona.quartz.nip01Core.crypto.verify import com.vitorpamplona.quartz.nip01Core.crypto.verify
import com.vitorpamplona.quartz.nip01Core.jackson.JacksonMapper 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.relay.client.NostrClient
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
@@ -163,7 +163,7 @@ class ThreadDualAxisChartAssemblerTest {
fun threadOrderTest() = fun threadOrderTest() =
runBlocking { runBlocking {
val eventArray = val eventArray =
JacksonMapper.mapper.readValue<List<Event>>(db) + Event.fromJson(header) JacksonMapper.mapper.readValue(db, jacksonTypeRefOf<List<Event>>()) + Event.fromJson(header)
var counter = 0 var counter = 0
eventArray.forEach { eventArray.forEach {
@@ -21,7 +21,7 @@
package com.vitorpamplona.amethyst.service.lnurl package com.vitorpamplona.amethyst.service.lnurl
import android.content.Context 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.R
import com.vitorpamplona.amethyst.service.HttpStatusMessages import com.vitorpamplona.amethyst.service.HttpStatusMessages
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
@@ -167,7 +167,7 @@ class LightningAddressResolver {
val errorMessage = val errorMessage =
runCatching { runCatching {
jacksonObjectMapper().readTree(body) ObjectMapper().readTree(body)
}.getOrNull()?.let { tree -> }.getOrNull()?.let { tree ->
val errorNode = tree.get("error") val errorNode = tree.get("error")
val messageNode = tree.get("message") val messageNode = tree.get("message")
@@ -218,7 +218,7 @@ class LightningAddressResolver {
context: Context, context: Context,
onZapRequestSent: (LnZapRequestEvent?) -> Unit = {}, onZapRequestSent: (LnZapRequestEvent?) -> Unit = {},
): String { ): String {
val mapper = jacksonObjectMapper() val mapper = ObjectMapper()
val lnurlpUrl = assembleUrl(lnAddress) val lnurlpUrl = assembleUrl(lnAddress)
@@ -26,7 +26,7 @@ import android.net.Uri
import android.provider.OpenableColumns import android.provider.OpenableColumns
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.core.net.toFile 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.R
import com.vitorpamplona.amethyst.service.HttpStatusMessages import com.vitorpamplona.amethyst.service.HttpStatusMessages
import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.checkNotInMainThread
@@ -209,7 +209,7 @@ class Nip96Uploader {
val errorMessage = val errorMessage =
try { try {
val tree = jacksonObjectMapper().readTree(msg) val tree = ObjectMapper().readTree(msg)
val status = tree.get("status")?.asText() val status = tree.get("status")?.asText()
val message = tree.get("message")?.asText() val message = tree.get("message")?.asText()
if (status == "error" && message != null) { if (status == "error" && message != null) {
@@ -21,7 +21,7 @@
package com.vitorpamplona.amethyst.ui.tor package com.vitorpamplona.amethyst.ui.tor
import com.fasterxml.jackson.databind.JsonNode 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 * 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. * circuit — i.e. a real bootstrap reached the guard-confirmation stage.
*/ */
object ArtiGuardState { object ArtiGuardState {
private val mapper = jacksonObjectMapper() private val mapper = ObjectMapper()
/** Convenience for tests/callers holding the raw file text. */ /** Convenience for tests/callers holding the raw file text. */
fun parse(json: String): JsonNode = mapper.readTree(json) fun parse(json: String): JsonNode = mapper.readTree(json)
@@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.tor
import android.content.Context import android.content.Context
import com.fasterxml.jackson.databind.JsonNode 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 com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -206,7 +206,7 @@ class TorService(
val file = guardsFile() val file = guardsFile()
if (!file.exists()) return null if (!file.exists()) return null
return try { return try {
jacksonObjectMapper().readTree(file) ObjectMapper().readTree(file)
} catch (e: Exception) { } catch (e: Exception) {
Log.w("TorService") { "Could not inspect guards.json: ${e.message}" } Log.w("TorService") { "Could not inspect guards.json: ${e.message}" }
null null
@@ -20,10 +20,10 @@
*/ */
package com.vitorpamplona.quartz.benchmark 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.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.jackson.JacksonMapper import com.vitorpamplona.quartz.nip01Core.jackson.JacksonMapper
import com.vitorpamplona.quartz.nip01Core.jackson.jacksonTypeRefOf
import com.vitorpamplona.quartz.utils.cache.LargeCache import com.vitorpamplona.quartz.utils.cache.LargeCache
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import java.util.function.Consumer import java.util.function.Consumer
@@ -35,8 +35,9 @@ open class BaseLargeCacheBenchmark {
// This file includes duplicates // This file includes duplicates
val fullDBInputStream = javaClass.classLoader!!.getResourceAsStream("nostr_vitor_startup_data.json.gz") val fullDBInputStream = javaClass.classLoader!!.getResourceAsStream("nostr_vitor_startup_data.json.gz")
return JacksonMapper.mapper.readValue<ArrayList<Event>>( return JacksonMapper.mapper.readValue(
GZIPInputStream(fullDBInputStream), GZIPInputStream(fullDBInputStream),
jacksonTypeRefOf<ArrayList<Event>>(),
) )
} }
} }
@@ -23,10 +23,10 @@ package com.vitorpamplona.quartz.benchmark
import androidx.benchmark.junit4.BenchmarkRule import androidx.benchmark.junit4.BenchmarkRule
import androidx.benchmark.junit4.measureRepeated import androidx.benchmark.junit4.measureRepeated
import androidx.test.ext.junit.runners.AndroidJUnit4 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.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.jackson.JacksonMapper import com.vitorpamplona.quartz.nip01Core.jackson.JacksonMapper
import com.vitorpamplona.quartz.nip01Core.jackson.jacksonTypeRefOf
import com.vitorpamplona.quartz.utils.cache.LargeCache import com.vitorpamplona.quartz.utils.cache.LargeCache
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Rule import org.junit.Rule
@@ -43,8 +43,9 @@ open class BaseCacheBenchmark {
// This file includes duplicates // This file includes duplicates
val fullDBInputStream = javaClass.classLoader?.getResourceAsStream("nostr_vitor_startup_data.json.gz") val fullDBInputStream = javaClass.classLoader?.getResourceAsStream("nostr_vitor_startup_data.json.gz")
return JacksonMapper.mapper.readValue<ArrayList<Event>>( return JacksonMapper.mapper.readValue(
GZIPInputStream(fullDBInputStream), GZIPInputStream(fullDBInputStream),
jacksonTypeRefOf<ArrayList<Event>>(),
) )
} }
@@ -23,7 +23,7 @@ package com.vitorpamplona.quartz.benchmark
import androidx.benchmark.junit4.BenchmarkRule import androidx.benchmark.junit4.BenchmarkRule
import androidx.benchmark.junit4.measureRepeated import androidx.benchmark.junit4.measureRepeated
import androidx.test.ext.junit.runners.AndroidJUnit4 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.nip01Core.core.Event
import com.vitorpamplona.quartz.utils.sha256.Sha256Hasher import com.vitorpamplona.quartz.utils.sha256.Sha256Hasher
import org.junit.Rule import org.junit.Rule
@@ -54,7 +54,7 @@ class EventCmdSerializerBenchmark {
@Test @Test
fun jsonStringEncoderJackson() { fun jsonStringEncoderJackson() {
val jsonMapper = jacksonObjectMapper() val jsonMapper = ObjectMapper()
benchmarkRule.measureRepeated { benchmarkRule.measureRepeated {
jsonMapper.writeValueAsString(specialEncoders) jsonMapper.writeValueAsString(specialEncoders)
} }
@@ -62,7 +62,7 @@ class EventCmdSerializerBenchmark {
@Test @Test
fun jsonStringEncoderSha256Jackson() { fun jsonStringEncoderSha256Jackson() {
val jsonMapper = jacksonObjectMapper() val jsonMapper = ObjectMapper()
benchmarkRule.measureRepeated { benchmarkRule.measureRepeated {
val digest = Sha256Hasher() val digest = Sha256Hasher()
digest.hash(jsonMapper.writeValueAsString(specialEncoders).toByteArray()) digest.hash(jsonMapper.writeValueAsString(specialEncoders).toByteArray())
@@ -20,7 +20,7 @@
*/ */
package com.vitorpamplona.amethyst.commons.service.lnurl 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.LnInvoiceUtil
import com.vitorpamplona.quartz.lightning.Lud06 import com.vitorpamplona.quartz.lightning.Lud06
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
@@ -48,7 +48,7 @@ import kotlin.coroutines.cancellation.CancellationException
class LightningAddressResolver( class LightningAddressResolver(
private val httpClient: OkHttpClient, private val httpClient: OkHttpClient,
) { ) {
private val mapper = jacksonObjectMapper() private val mapper = ObjectMapper()
/** /**
* Result of resolving a lightning address to a BOLT11 invoice. * Result of resolving a lightning address to a BOLT11 invoice.
@@ -20,7 +20,7 @@
*/ */
package com.vitorpamplona.amethyst.commons.service.lnurl 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.LnurlEndpointCache
import com.vitorpamplona.quartz.nip57Zaps.validate.LnurlEndpointInfo import com.vitorpamplona.quartz.nip57Zaps.validate.LnurlEndpointInfo
import com.vitorpamplona.quartz.nip57Zaps.validate.LnurlEndpointResolver import com.vitorpamplona.quartz.nip57Zaps.validate.LnurlEndpointResolver
@@ -47,7 +47,7 @@ import kotlin.coroutines.cancellation.CancellationException
class OkHttpLnurlEndpointResolver( class OkHttpLnurlEndpointResolver(
private val okHttpClient: (String) -> OkHttpClient, private val okHttpClient: (String) -> OkHttpClient,
) : LnurlEndpointResolver { ) : LnurlEndpointResolver {
private val mapper = jacksonObjectMapper() private val mapper = ObjectMapper()
override suspend fun resolve(lnurlpUrl: String): LnurlEndpointInfo? = LnurlEndpointCache.getOrFetch(lnurlpUrl, ::fetch) override suspend fun resolve(lnurlpUrl: String): LnurlEndpointInfo? = LnurlEndpointCache.getOrFetch(lnurlpUrl, ::fetch)
+2
View File
@@ -38,6 +38,7 @@ firebaseBom = "34.19.0"
fragmentKtx = "1.9.0" fragmentKtx = "1.9.0"
gms = "4.5.0" gms = "4.5.0"
healthConnect = "1.1.0" healthConnect = "1.1.0"
jackson = "2.22.2"
jacksonModuleKotlin = "2.22.2" jacksonModuleKotlin = "2.22.2"
javaKeyring = "1.0.4" javaKeyring = "1.0.4"
kmpTorRuntime = "2.6.0" 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-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-language-id = { group = "com.google.mlkit", name = "language-id", version.ref = "languageId" }
google-mlkit-translate = { group = "com.google.mlkit", name = "translate", version.ref = "translate" } 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" } 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" } 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" } kmp-tor-runtime = { group = "io.matthewnelson.kmp-tor", name = "runtime", version.ref = "kmpTorRuntime" }
+10 -2
View File
@@ -177,8 +177,16 @@ kotlin {
dependsOn(commonMain.get()) dependsOn(commonMain.get())
dependencies { dependencies {
// Performant Parser of JSONs into Events // Performant Parser of JSONs into Events.
api(libs.jackson.module.kotlin) //
// 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 // Websockets API
implementation(libs.okhttp) implementation(libs.okhttp)
@@ -21,10 +21,10 @@
package com.vitorpamplona.quartz package com.vitorpamplona.quartz
import androidx.test.ext.junit.runners.AndroidJUnit4 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.Event
import com.vitorpamplona.quartz.nip01Core.crypto.verify import com.vitorpamplona.quartz.nip01Core.crypto.verify
import com.vitorpamplona.quartz.nip01Core.jackson.JacksonMapper import com.vitorpamplona.quartz.nip01Core.jackson.JacksonMapper
import com.vitorpamplona.quartz.nip01Core.jackson.jacksonTypeRefOf
import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertEquals
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
@@ -41,8 +41,9 @@ class LargeDBSignatureCheck {
val fullDBInputStream = javaClass.classLoader?.getResourceAsStream("nostr_vitor_short.json") val fullDBInputStream = javaClass.classLoader?.getResourceAsStream("nostr_vitor_short.json")
val eventArray = val eventArray =
JacksonMapper.mapper.readValue<ArrayList<Event>>( JacksonMapper.mapper.readValue(
InputStreamReader(fullDBInputStream), InputStreamReader(fullDBInputStream),
jacksonTypeRefOf<ArrayList<Event>>(),
) as List<Event> ) as List<Event>
var counter = 0 var counter = 0
@@ -61,8 +62,9 @@ class LargeDBSignatureCheck {
val fullDBInputStream = javaClass.classLoader?.getResourceAsStream("nostr_vitor_startup_data.json.gz") val fullDBInputStream = javaClass.classLoader?.getResourceAsStream("nostr_vitor_startup_data.json.gz")
val eventArray = val eventArray =
JacksonMapper.mapper.readValue<ArrayList<Event>>( JacksonMapper.mapper.readValue(
GZIPInputStream(fullDBInputStream), GZIPInputStream(fullDBInputStream),
jacksonTypeRefOf<ArrayList<Event>>(),
) as List<Event> ) as List<Event>
var counter = 0 var counter = 0
@@ -21,7 +21,7 @@
package com.vitorpamplona.quartz.nip06KeyDerivation package com.vitorpamplona.quartz.nip06KeyDerivation
import androidx.test.ext.junit.runners.AndroidJUnit4 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.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.Hex
import com.vitorpamplona.quartz.utils.RandomInstance import com.vitorpamplona.quartz.utils.RandomInstance
@@ -34,7 +34,7 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class Bip39MnemonicsTest { class Bip39MnemonicsTest {
private val tests = private val tests =
jacksonObjectMapper() ObjectMapper()
.readTree(javaClass.classLoader?.getResourceAsStream("bip39.vectors.json")) .readTree(javaClass.classLoader?.getResourceAsStream("bip39.vectors.json"))
@Test @Test
@@ -26,9 +26,8 @@ import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.databind.node.ArrayNode import com.fasterxml.jackson.databind.node.ArrayNode
import com.fasterxml.jackson.databind.node.ObjectNode 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.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.IntentResult
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.results.IntentResultJsonDeserializer import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.results.IntentResultJsonDeserializer
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.results.IntentResultJsonSerializer import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.results.IntentResultJsonSerializer
@@ -39,7 +38,7 @@ import java.io.InputStream
object JsonMapperNip55 { object JsonMapperNip55 {
val defaultMapper: ObjectMapper = val defaultMapper: ObjectMapper =
jacksonObjectMapper() ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature()) .enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
.setDefaultPrettyPrinter(InliningTagArrayPrettyPrinter()) .setDefaultPrettyPrinter(InliningTagArrayPrettyPrinter())
@@ -51,9 +50,9 @@ object JsonMapperNip55 {
.addSerializer(Permission::class.java, PermissionSerializer()), .addSerializer(Permission::class.java, PermissionSerializer()),
) )
inline fun <reified T> fromJsonTo(json: String): T = defaultMapper.readValue<T>(json) inline fun <reified T> fromJsonTo(json: String): T = defaultMapper.readValue(json, jacksonTypeRefOf<T>())
inline fun <reified T> fromJsonTo(json: InputStream): T = defaultMapper.readValue<T>(json) inline fun <reified T> fromJsonTo(json: InputStream): T = defaultMapper.readValue(json, jacksonTypeRefOf<T>())
fun toJson(event: ArrayNode): String = defaultMapper.writeValueAsString(event) fun toJson(event: ArrayNode): String = defaultMapper.writeValueAsString(event)
@@ -23,12 +23,10 @@ package com.vitorpamplona.quartz.nip01Core.jackson
import com.fasterxml.jackson.core.json.JsonReadFeature import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.JavaType import com.fasterxml.jackson.databind.JavaType
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.databind.node.ArrayNode import com.fasterxml.jackson.databind.node.ArrayNode
import com.fasterxml.jackson.databind.node.ObjectNode 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.Event
import com.vitorpamplona.quartz.nip01Core.core.OptimizedSerializable import com.vitorpamplona.quartz.nip01Core.core.OptimizedSerializable
import com.vitorpamplona.quartz.nip01Core.core.RawJson import com.vitorpamplona.quartz.nip01Core.core.RawJson
@@ -64,7 +62,7 @@ class JacksonMapper {
val defaultPrettyPrinter = InliningTagArrayPrettyPrinter() val defaultPrettyPrinter = InliningTagArrayPrettyPrinter()
val mapper = val mapper =
jacksonObjectMapper() ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.configure(DeserializationFeature.FAIL_ON_TRAILING_TOKENS, false) .configure(DeserializationFeature.FAIL_ON_TRAILING_TOKENS, false)
.configure(DeserializationFeature.UNWRAP_ROOT_VALUE, false) .configure(DeserializationFeature.UNWRAP_ROOT_VALUE, false)
@@ -104,13 +102,13 @@ class JacksonMapper {
/** /**
* Shortcuts * Shortcuts
*/ */
val eventTypeInstance: JavaType = mapper.typeFactory.constructType(jacksonTypeRef<Event>()) val eventTypeInstance: JavaType = mapper.typeFactory.constructType(jacksonTypeRefOf<Event>())
val tagArrayTypeInstance: JavaType = mapper.typeFactory.constructType(jacksonTypeRef<TagArray>()) val tagArrayTypeInstance: JavaType = mapper.typeFactory.constructType(jacksonTypeRefOf<TagArray>())
val rumorTypeInstance: JavaType = mapper.typeFactory.constructType(jacksonTypeRef<Rumor>()) val rumorTypeInstance: JavaType = mapper.typeFactory.constructType(jacksonTypeRefOf<Rumor>())
val eventTemplateTypeInstance: JavaType = mapper.typeFactory.constructType(jacksonTypeRef<EventTemplate<Event>>()) val eventTemplateTypeInstance: JavaType = mapper.typeFactory.constructType(jacksonTypeRefOf<EventTemplate<Event>>())
val eventListTypeInstance: JavaType = mapper.typeFactory.constructType(jacksonTypeRef<List<Event>>()) val eventListTypeInstance: JavaType = mapper.typeFactory.constructType(jacksonTypeRefOf<List<Event>>())
val messageTypeInstance: JavaType = mapper.typeFactory.constructType(jacksonTypeRef<Message>()) val messageTypeInstance: JavaType = mapper.typeFactory.constructType(jacksonTypeRefOf<Message>())
val commandTypeInstance: JavaType = mapper.typeFactory.constructType(jacksonTypeRef<Command>()) val commandTypeInstance: JavaType = mapper.typeFactory.constructType(jacksonTypeRefOf<Command>())
fun fromJson(json: String): Event = mapper.readValue(json, eventTypeInstance) fun fromJson(json: String): Event = mapper.readValue(json, eventTypeInstance)
@@ -169,12 +167,12 @@ class JacksonMapper {
inline fun <reified T : OptimizedSerializable> fromJsonTo(json: String): T { inline fun <reified T : OptimizedSerializable> fromJsonTo(json: String): T {
checkRegistered(T::class) checkRegistered(T::class)
return mapper.readValue<T>(json) return mapper.readValue(json, jacksonTypeRefOf<T>())
} }
inline fun <reified T : OptimizedSerializable> fromJsonTo(json: InputStream): T { inline fun <reified T : OptimizedSerializable> fromJsonTo(json: InputStream): T {
checkRegistered(T::class) checkRegistered(T::class)
return mapper.readValue<T>(json) return mapper.readValue(json, jacksonTypeRefOf<T>())
} }
fun toJson(event: Event): String = EventManualSerializer.toJson(event.id, event.pubKey, event.createdAt, event.kind, event.tags, event.content, event.sig) fun toJson(event: Event): String = EventManualSerializer.toJson(event.id, event.pubKey, event.createdAt, event.kind, event.tags, event.content, event.sig)
@@ -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<Event>`.
*
* 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<Event>` 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 <reified T> jacksonTypeRefOf(): TypeReference<T> = object : TypeReference<T>() {}
@@ -20,13 +20,13 @@
*/ */
package com.vitorpamplona.quartz.nip01Core.jackson 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.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
class InliningTagArrayPrettyPrinterTest { class InliningTagArrayPrettyPrinterTest {
val mapper = val mapper =
jacksonObjectMapper().apply { ObjectMapper().apply {
setDefaultPrettyPrinter(InliningTagArrayPrettyPrinter()) setDefaultPrettyPrinter(InliningTagArrayPrettyPrinter())
} }
@@ -21,7 +21,6 @@
package com.vitorpamplona.quic.interop package com.vitorpamplona.quic.interop
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.quic.connection.EncryptionLevel import com.vitorpamplona.quic.connection.EncryptionLevel
import com.vitorpamplona.quic.observability.QlogObserver import com.vitorpamplona.quic.observability.QlogObserver
import java.io.BufferedWriter import java.io.BufferedWriter
@@ -295,7 +294,7 @@ class QlogWriter(
} }
companion object { companion object {
private val DEFAULT_MAPPER: ObjectMapper = jacksonObjectMapper() private val DEFAULT_MAPPER: ObjectMapper = ObjectMapper()
private fun packetTypeFor(level: EncryptionLevel): String = private fun packetTypeFor(level: EncryptionLevel): String =
when (level) { when (level) {
@@ -20,7 +20,7 @@
*/ */
package com.vitorpamplona.quic.interop 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 com.vitorpamplona.quic.connection.EncryptionLevel
import org.junit.Test import org.junit.Test
import java.io.File import java.io.File
@@ -73,7 +73,7 @@ class QlogWriterTest {
val lines = tmp.readLines().filter { it.isNotBlank() } val lines = tmp.readLines().filter { it.isNotBlank() }
assertTrue(lines.size >= 12, "expected >= 12 lines (header + at least 11 events) but got ${lines.size}") 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. // Line 1: qlog header.
val header = mapper.readTree(lines[0]) val header = mapper.readTree(lines[0])
@@ -128,7 +128,7 @@ class QlogWriterTest {
w.onAlpnNegotiated("h3") w.onAlpnNegotiated("h3")
} }
val lines = tmp.readLines().filter { it.isNotBlank() } val lines = tmp.readLines().filter { it.isNotBlank() }
val mapper = jacksonObjectMapper() val mapper = ObjectMapper()
val event = mapper.readTree(lines[1]) val event = mapper.readTree(lines[1])
assertEquals(50L, event.get("time").asLong(), "time must be relative to constructor (1050 - 1000)") 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 -> QlogWriter(tmp, odcidHex = "00").use { w ->
w.onPacketSent(EncryptionLevel.INITIAL, 0, 1200, emptyList()) w.onPacketSent(EncryptionLevel.INITIAL, 0, 1200, emptyList())
} }
val mapper = jacksonObjectMapper() val mapper = ObjectMapper()
val lines = tmp.readLines().filter { it.isNotBlank() } val lines = tmp.readLines().filter { it.isNotBlank() }
val frames = mapper.readTree(lines[1]).get("data").get("frames") val frames = mapper.readTree(lines[1]).get("data").get("frames")
assertTrue(frames.isArray, "frames must be an array even when empty") assertTrue(frames.isArray, "frames must be an array even when empty")