Code review:

- Expose a nullable descriptor
- Log the JSON element kind instead of the raw, network-sourced value.
- drop birthday happy-path tests duplicated by UpdateMetadataTest
This commit is contained in:
davotoula
2026-06-07 23:18:57 +02:00
parent 39531b85fb
commit 0107808ef6
2 changed files with 13 additions and 38 deletions
@@ -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<Birthday?> {
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<Birthday?> {
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
}
@@ -75,41 +75,12 @@ class BirthdayTolerantSerializerTest {
}
}
@Test
fun specObjectBirthdayStillParses() {
val meta = metaWith("""{"name":"A","birthday":{"year":1990,"month":6,"day":15}}""").contactMetaData()
assertIs<UserMetadata>(meta)
val birthday = meta.birthday
assertIs<Birthday>(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<UserMetadata>(meta)
val birthday = meta.birthday
assertIs<Birthday>(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<UserMetadata>(meta)
val serialized = JsonMapper.toJson(meta)
val reparsed = JsonMapper.fromJson<UserMetadata>(serialized)
val birthday = reparsed.birthday
assertIs<Birthday>(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()