mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-09-20 08:45:50 +01:00
Add base kotlinx serialization tooling.
This commit is contained in:
+26
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.core.util.serialization
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import org.signal.core.util.Base64
|
||||
|
||||
class ByteArrayToBase64NoPaddingSerializer : KSerializer<ByteArray> {
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ByteArray", PrimitiveKind.STRING)
|
||||
|
||||
override fun deserialize(decoder: Decoder): ByteArray {
|
||||
return Base64.decode(decoder.decodeString())
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: ByteArray) {
|
||||
encoder.encodeString(Base64.encodeWithoutPadding(value))
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.core.util.serialization
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey
|
||||
|
||||
class ECPublicKeyToBase64NoPaddingSerializer : KSerializer<ECPublicKey> {
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ECPublicKey", PrimitiveKind.STRING)
|
||||
|
||||
override fun deserialize(decoder: Decoder): ECPublicKey {
|
||||
return ECPublicKey(Base64.decode(decoder.decodeString()))
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: ECPublicKey) {
|
||||
encoder.encodeString(Base64.encodeWithoutPadding(value.serialize()))
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.core.util.serialization
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.libsignal.protocol.IdentityKey
|
||||
|
||||
class IdentityKeyToBase64NoPaddingSerializer : KSerializer<IdentityKey> {
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("IdentityKey", PrimitiveKind.STRING)
|
||||
|
||||
override fun deserialize(decoder: Decoder): IdentityKey {
|
||||
return IdentityKey(Base64.decode(decoder.decodeString()), 0)
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: IdentityKey) {
|
||||
encoder.encodeString(Base64.encodeWithoutPadding(value.serialize()))
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.core.util.serialization
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.libsignal.protocol.kem.KEMPublicKey
|
||||
|
||||
class KEMPublicKeyToBase64NoPaddingSerializer : KSerializer<KEMPublicKey> {
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("KEMPublicKey", PrimitiveKind.STRING)
|
||||
|
||||
override fun deserialize(decoder: Decoder): KEMPublicKey {
|
||||
return KEMPublicKey(Base64.decode(decoder.decodeString()))
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: KEMPublicKey) {
|
||||
encoder.encodeString(Base64.encodeWithoutPadding(value.serialize()))
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.core.util.serialization
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import org.signal.core.models.MasterKey
|
||||
import org.signal.core.util.Base64
|
||||
|
||||
class MasterKeyToBase64Serializer : KSerializer<MasterKey> {
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("MasterKey", PrimitiveKind.STRING)
|
||||
|
||||
override fun deserialize(decoder: Decoder): MasterKey {
|
||||
return MasterKey(Base64.decode(decoder.decodeString()))
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: MasterKey) {
|
||||
encoder.encodeString(Base64.encodeWithPadding(value.serialize()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.core.util.serialization
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import org.signal.core.models.ServiceId.PNI
|
||||
|
||||
class PniSerializer : KSerializer<PNI> {
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("PNI", PrimitiveKind.STRING)
|
||||
|
||||
override fun deserialize(decoder: Decoder): PNI {
|
||||
return PNI.parseOrThrow(decoder.decodeString())
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: PNI) {
|
||||
encoder.encodeString(value.toString())
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.core.util.serialization
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import org.signal.core.models.ServiceId
|
||||
|
||||
class ServiceIdSerializer : KSerializer<ServiceId> {
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ServiceId", PrimitiveKind.STRING)
|
||||
|
||||
override fun deserialize(decoder: Decoder): ServiceId {
|
||||
return ServiceId.parseOrThrow(decoder.decodeString())
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: ServiceId) {
|
||||
encoder.encodeString(value.toString())
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,18 @@ import kotlinx.serialization.json.Json
|
||||
*/
|
||||
object SignalJson {
|
||||
|
||||
val json = Json { ignoreUnknownKeys = true }
|
||||
/**
|
||||
* The JSON instance to use by default.
|
||||
*/
|
||||
val json = Json {
|
||||
ignoreUnknownKeys = true // If the service adds a field we don't track in our data model, ignore it during parsing instead of throwing an exception. I have no idea why this isn't the default.
|
||||
encodeDefaults = true // If we have a default value for an arg in the constructor, always include it in the JSON output, even if we don't set it explicitly.
|
||||
}
|
||||
|
||||
/**
|
||||
* [json], but null-valued properties are omitted entirely rather than written as `null`. Useful for specific endpoints.
|
||||
*/
|
||||
val jsonOmitNulls = Json(json) { explicitNulls = false }
|
||||
|
||||
inline fun <reified T> encode(input: T): Either<EncodeError, String> = either {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.core.util.serialization
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import org.signal.core.util.UuidUtil
|
||||
import java.util.UUID
|
||||
|
||||
class UuidSerializer : KSerializer<UUID> {
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)
|
||||
|
||||
override fun deserialize(decoder: Decoder): UUID {
|
||||
return UuidUtil.parseOrThrow(decoder.decodeString())
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: UUID) {
|
||||
encoder.encodeString(value.toString())
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.core.util.serialization.testutil
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
|
||||
/**
|
||||
* Verifies that a model migrated from Jackson to kotlinx.serialization still produces and accepts the exact same JSON.
|
||||
*
|
||||
* The intended workflow when migrating a model is:
|
||||
* 1. Before touching the model, capture its current output via [LegacyJacksonJson] and paste the string into a test.
|
||||
* 2. Migrate the model to `@Serializable`.
|
||||
* 3. Assert with [assertMatchesGolden], which fails if either direction drifted.
|
||||
*/
|
||||
object JsonGolden {
|
||||
|
||||
/**
|
||||
* Asserts that [value] encodes to JSON equivalent to [golden], and that decoding [golden] round-trips back to the
|
||||
* same JSON. Object key order is ignored; everything else must match exactly.
|
||||
*/
|
||||
fun <T> assertMatchesGolden(serializer: KSerializer<T>, value: T, golden: String, json: Json) {
|
||||
val encoded = json.encodeToString(serializer, value)
|
||||
assertJsonEquals(golden, encoded, "Encoding does not match the golden JSON.")
|
||||
|
||||
val reEncoded = json.encodeToString(serializer, json.decodeFromString(serializer, golden))
|
||||
assertJsonEquals(golden, reEncoded, "Decoding the golden JSON and re-encoding it does not match.")
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that two JSON strings are structurally equal, ignoring object key order.
|
||||
*/
|
||||
fun assertJsonEquals(expected: String, actual: String, message: String = "JSON does not match.") {
|
||||
val expectedElement = Json.parseToJsonElement(expected)
|
||||
val actualElement = Json.parseToJsonElement(actual)
|
||||
|
||||
if (expectedElement != actualElement) {
|
||||
throw AssertionError("$message\n Expected: ${expectedElement.canonical()}\n Actual: ${actualElement.canonical()}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun JsonElement.canonical(): String = Json { prettyPrint = false }.encodeToString(JsonElement.serializer(), this)
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.core.util.serialization.testutil
|
||||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.SerializationFeature
|
||||
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
|
||||
|
||||
/**
|
||||
* Replicas of the two legacy Jackson `ObjectMapper` configurations in the codebase, so that tests can capture the JSON
|
||||
* a model produced before it was migrated to kotlinx.serialization.
|
||||
*
|
||||
* These exist purely to support the Jackson removal and should be deleted along with `JsonUtils` and `JsonUtil`.
|
||||
*/
|
||||
object LegacyJacksonJson {
|
||||
|
||||
/** Mirrors `org.signal.core.util.JsonUtils`, which is used for locally persisted data. Note the enum handling. */
|
||||
val storageMapper: ObjectMapper = ObjectMapper().apply {
|
||||
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||
enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING)
|
||||
enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING)
|
||||
registerKotlinModule()
|
||||
}
|
||||
|
||||
/** Mirrors `org.signal.network.util.JsonUtil`, which is used for network wire formats. */
|
||||
val networkMapper: ObjectMapper = ObjectMapper().apply {
|
||||
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||
registerKotlinModule()
|
||||
}
|
||||
|
||||
fun encodeForStorage(value: Any): String = storageMapper.writeValueAsString(value)
|
||||
|
||||
fun encodeForNetwork(value: Any): String = networkMapper.writeValueAsString(value)
|
||||
}
|
||||
Reference in New Issue
Block a user