diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/BirthdayTolerantSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/BirthdayTolerantSerializer.kt index df549128e4..5e04867275 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/BirthdayTolerantSerializer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/BirthdayTolerantSerializer.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.nip01Core.metadata import com.vitorpamplona.quartz.utils.Log import kotlinx.serialization.KSerializer import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.nullable import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.JsonDecoder @@ -48,7 +49,9 @@ import kotlinx.serialization.json.JsonObject object BirthdayTolerantSerializer : KSerializer { private val delegate = Birthday.serializer() - override val descriptor: SerialDescriptor = delegate.descriptor + // Nullable serializer ⇒ nullable descriptor, so the framework's metadata stays + // honest even on code paths that consult it (e.g. coerceInputValues). + override val descriptor: SerialDescriptor = delegate.descriptor.nullable override fun deserialize(decoder: Decoder): Birthday? { require(decoder is JsonDecoder) { "This serializer can only be used with Json format" } @@ -56,8 +59,9 @@ object BirthdayTolerantSerializer : KSerializer { val element = decoder.decodeJsonElement() if (element !is JsonObject) { // Non-spec birthday (e.g. Ditto's "10-24" string). Ignore it rather than - // failing the whole profile parse. - Log.w("BirthdayTolerantSerializer") { "Ignoring non-object birthday: $element" } + // failing the whole profile parse. Log the JSON kind only, not the raw + // (untrusted, network-sourced) value. + Log.w("BirthdayTolerantSerializer") { "Ignoring non-object birthday (${element::class.simpleName})" } return null } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/BirthdayTolerantSerializerTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/BirthdayTolerantSerializerTest.kt index 1495b3262f..839545b344 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/BirthdayTolerantSerializerTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/BirthdayTolerantSerializerTest.kt @@ -75,41 +75,12 @@ class BirthdayTolerantSerializerTest { } } - @Test - fun specObjectBirthdayStillParses() { - val meta = metaWith("""{"name":"A","birthday":{"year":1990,"month":6,"day":15}}""").contactMetaData() - assertIs(meta) - val birthday = meta.birthday - assertIs(birthday) - assertEquals(1990, birthday.year) - assertEquals(6, birthday.month) - assertEquals(15, birthday.day) - } - - @Test - fun partialObjectBirthdayStillParses() { - val meta = metaWith("""{"name":"A","birthday":{"month":6,"day":15}}""").contactMetaData() - assertIs(meta) - val birthday = meta.birthday - assertIs(birthday) - assertNull(birthday.year) - assertEquals(6, birthday.month) - assertEquals(15, birthday.day) - } - - @Test - fun objectBirthdayRoundTrips() { - val meta = metaWith("""{"name":"A","birthday":{"year":1990,"month":6,"day":15}}""").contactMetaData() - assertIs(meta) - val serialized = JsonMapper.toJson(meta) - val reparsed = JsonMapper.fromJson(serialized) - val birthday = reparsed.birthday - assertIs(birthday) - assertEquals(1990, birthday.year) - assertEquals(6, birthday.month) - assertEquals(15, birthday.day) - } - + /** + * End-to-end check that a dropped birthday does not leak back into the + * serialized profile. The omission itself is the decoder's default-null + * suppression (encodeDefaults stays false on JsonMapper), not the serializer — + * this just pins the real-world JsonMapper output. + */ @Test fun nullBirthdayIsOmittedOnSerialization() { val meta = metaWith("""{"name":"A","birthday":"10-24"}""").contactMetaData()