janvladimirmostert
01/29/2023, 7:30 PMemail
if it's serialized for the purpose of logging, but not if it's serialized for the purpose of sending it over the wire, so I'm looking for a way to swop out Serializers depending on context.
Example:
@JvmInline
@Serializable
value class Email(override val value: String) : Sensitive {
override fun mask(): String {
return value.split("@").let {
"_".repeat(it.first().length) + "@" + it.last()
}
}
object SensitiveSerializer : KSerializer<Email> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Email", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): Email = Email(decoder.decodeString())
override fun serialize(encoder: Encoder, value: Email) = encoder.encodeString(value.mask())
}
}
val wireMapper = Json {
prettyPrint = true
ignoreUnknownKeys = false
}
val sensitiveMapper = Json {
prettyPrint = true
ignoreUnknownKeys = false
serializersModule = SerializersModule {
this.contextual(Email::class, Email.SensitiveSerializer)
}
}
val testMasking = QuoteRequestTeamB.QuoteV1(email = Email("<mailto:test@gmail.com|test@gmail.com>"))
// expected { "email": "<mailto:____@gmail.com|____@gmail.com>" }
sensitiveMapper.encodeToString(testMasking).also(::println)
// expected { "email": "<mailto:test@gmail.com|test@gmail.com>" }
wireMapper.encodeToString(testMasking).also(::println)
If I replace the @Serializable
with @Serializable(with = Email.SensitiveSerializer)
, then it always masks and if I remove it, it never masks which mens the serializersModule is not working here.
Any idea what I'm missing?Adam S
01/29/2023, 7:46 PM@Contextual
https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serializers.md#contextual-serialization
What you could also do is create a custom encoder. This won’t be too difficult - use StringFormat and as it’s an interface you can delegate to a Json
instance. And then you just need to intercept any properties you want to mask.
edit: Hmm maybe not StringFormat… I’ll have another lookjanvladimirmostert
01/29/2023, 8:00 PM@Serializable
data class QuoteV1(
@Contextual
val email: Email
) {
override fun toString(): String {
return sensitiveMapper.encodeToString(this)
}
}
works for that one field, but this means all fields will need to be annotated with @Contextual
which is exactly what I'm trying to avoid by using the value classes
I guess it's a start, at least I know why it wasn't workingtypealias DateAsLong = @Serializable(DateAsLongSerializer::class) Date
typealias DateAsText = @Serializable(DateAsSimpleTextSerializer::class) Date
@Serializable
class ProgrammingLanguage(val stableReleaseDate: DateAsText, val lastReleaseTimestamp: DateAsLong)
fun main() {
val format = SimpleDateFormat("yyyy-MM-ddX")
val data = ProgrammingLanguage(format.parse("2016-02-15+00"), format.parse("2022-07-07+00"))
println(Json.encodeToString(data))
}
Adam S
01/29/2023, 8:08 PMjsonMasked
Json encoder instance is used during encoding, and if so, then it masks the dataephemient
01/29/2023, 10:47 PMval jsonUnmasked = Json {
...
}
val jsonMasked = Json {
serializersModule = SerializersModule {
contextual(MaskedSerializer)
}
}
of course you could flip the logic around if you wanted it to default to masked unless a specific contextual serializer is givenjanvladimirmostert
01/29/2023, 11:02 PMobject RequestSerializer : JsonContentPolymorphicSerializer
which doesn't support overriding anything other than the deserializer
seems like I'll need to write my own from scratch to support both polymorphic + masking serializing + deserializing@JvmInline
@Serializable(with = Email.SensitiveSerializer::class)
value class Email(override val value: String) : Sensitive {
override fun mask(): String {
return value.split("@").let {
"_".repeat(it.first().length) + "@" + it.last()
}
}
object SensitiveSerializer : KSerializer<Email> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Email", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): Email = Email(decoder.decodeString())
override fun serialize(encoder: Encoder, email: Email) {
if (encoder is JsonEncoder && encoder.json == sensitiveMapper) {
encoder.encodeString(email.mask())
} else {
encoder.encodeString(email.value)
}
}
}
}
ephemient
01/29/2023, 11:50 PMJson(from = json)
to behave like json
, but it won't with thatjanvladimirmostert
01/29/2023, 11:50 PMoverride fun serialize(encoder: Encoder, value: Email) {
if (encoder is JsonEncoder && encoder.json != wireMapper) {
encoder.encodeString(value.mask())
} else {
encoder.encodeString(value.value)
}
}
Json.encodeToString(myTestData).also(::println)
now masks as well which is perfect
I'll build the correct serializer into the HTTP function so that it doesn't mask thereephemient
01/29/2023, 11:54 PM.json ==
is badjanvladimirmostert
01/29/2023, 11:56 PM@JvmInline
@Serializable(with = Email.SensitiveSerializer::class)
value class Email(override val value: String) : Sensitive {
override fun mask(): String {
return value.split("@").let {
value.first() + "_".repeat(max(0, it.first().length - 1)) + "@" + it.last()
}
}
@ExperimentalSerializationApi
object SensitiveSerializer : KSerializer<Email> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Email", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): Email = Email(decoder.decodeString())
override fun serialize(encoder: Encoder, value: Email) {
encoder.serializersModule.getContextual(Sensitive::class)?.let {
encoder.encodeString(value.mask())
} ?: encoder.encodeString(value.value)
}
}
override fun toString(): String {
return mask()
}
}
val wireMapper = Json {
prettyPrint = true
ignoreUnknownKeys = false
}
val sensitiveMapper = Json {
prettyPrint = true
ignoreUnknownKeys = false
serializersModule = SerializersModule {
this.contextual(MaskedSerializer)
}
}
pdvrieze
02/01/2023, 2:38 PMis
to determine whether the format is this special format (the same case you can special case the Jon
format). It can then read the property and do what is appropriate.
This technique could be used for any case that needs dynamic serialization. The advantage is that you don't need to rely on the right serializer being added to the context.