Zyle Moore
11/19/2024, 3:32 AMephemient
11/19/2024, 5:54 AMdecodeSequentially() = true
only works with decodeCollectionSize() != -1
ephemient
11/19/2024, 5:55 AMephemient
11/19/2024, 5:56 AMZyle Moore
11/19/2024, 7:21 AMZyle Moore
11/19/2024, 7:56 AMDaniel Pitts
11/19/2024, 3:29 PMBen Woodworth
11/19/2024, 7:16 PMannotations
, have a @SerialInfo
-marked BathesdaNullTerminatedString
annotation, and maybe a SerialDescriptor.isBathesdaNullTerminatedString
extension property that the encoder/decoder use. (And definitely pick better names for these. I haven't played much of Elder Scrolls 😅)Zyle Moore
11/19/2024, 9:56 PMZyle Moore
11/23/2024, 2:01 AM@Target
annotation. The documentation mentions it's recommended, but is it actually required?Ben Woodworth
11/23/2024, 2:09 AMBen Woodworth
11/23/2024, 2:12 AMZyle Moore
11/23/2024, 3:58 AMencodeElement
, would I need to implement CompositeEncoder
instead of AbstractEncoder()
, because the abstract one has a final
implementation of encodeStringElement
? Otherwise, which component has the context to be able to encode an @NullTerminated val someString: String
?Ben Woodworth
11/23/2024, 4:17 AMZyle Moore
11/23/2024, 4:26 AMZyle Moore
11/23/2024, 5:26 AMZyle Moore
11/23/2024, 5:36 AMencodeSerializableValue
. Two encoded ints would have a length of 8, so it would need to encode [8, [4 bytes], [4 bytes]]
. My current implementation is to serialize this struct independently of the "main" Encoding. Some structs have a maximum size, but I need to encode the actual size, before the actual data. The second Encoder would receive repeat(2) { encodeInt }
, the initial Encoder would receive repeat(9) { encodeByte }
Are there common pitfalls to doing this this way? Mostly in the realm of newing up serializers with separate Encoders mid-encoding. Or is there a more conventional way of prepending the expected payload size?Ben Woodworth
11/23/2024, 5:26 PMephemient
11/24/2024, 2:47 AMephemient
11/24/2024, 2:51 AMZyle Moore
11/29/2024, 10:20 PMserializersModule
to determine which serializers to use
• Format creates (En|De)coder
instance, which uses a (Read|Writ)er
wrapping/returning string/byte array
• Encoder uses format's serializersModule
• Encoder primarily uses SerialDescriptor
and SerialInfo
annotations for decision making (like the one mentioned earlier in the thread)
• Encoder overrides primitive and serializable encoding methods
• Encoder manipulates the state of the Writer
, using format-specific types and constructs in the Writer
• Writer will, based on the request, manipulate the underlying data structure (string builder, byte array output)
• Depending on the format-specific type, additional data may be written before or after the call-specific data.
◦ For example, in Protobuf, encodeInt
calls encodeTaggedInt
which actually write two ints to the underlying data structure; so a call to encodeInt(26)
would instead be translated to encodeInt(some tag) + encodeInt(26)
I understand the Reader/Writer as an Anti-Corruption Layer between Kotlin Serialization and whatever the underlying structure is. But isn't this last step also a serialization?
In order to encode this higher-level construct (tagged int, rather than primitive int) it still needs to determine the order of components (first Tag, then Value), and encode them in the data structure (int, int).
As an example, Protobuf could decide tomorrow that Tags are now Shorts, and Values are now little-endian. but technically, the outer most call from the Format doesn't know about that. It's still just going to boil down to encodeInt
calls from the Serializer's perspective, and the Encoder is going to translate those to encodeTaggedInt
calls, which gets translated to whatever the format needs.Zyle Moore
12/01/2024, 5:39 AMencodeInt
implementation doesn't write a signed int, but instead an unsigned int as an unquoted string. Here, it seems like the Encoder is the correct place to put format-specific primitive encoding decisions, going as far as having a separate encoder for format-specific representations of primitive data types.
However, the Serializable value itself can be represented differently with different Serializers. Some of the examples for a Color
type are "#fff"
, {"r":0.1,"g":1.0,"b":0.5}
, and red
. These Serializers would vary in their calls to the Encoder, depending on the serial representation they provide. The Json example above would start with a beginStructure
call probably, the string could be encodeString
or encodeInline
or repeated calls to encodeByte
or a custom encodeHex
, or an encodeInt
that actually calls an encodeQuotedHexString
.
So my question is, if format-specific data types require format-specific representations, where do we draw the line between the responsibility of format-specific decisions? How would I know to choose
override fun serialize(encoder: Encoder, value: NullTerminatedString) {
encoder.encodeString(value.string)
}
...
override fun encodeString(value: String) {
output.writeString(value)
output.writeByte(0)
}
over
override fun serialize(encoder: Encoder, value: NullTerminatedString) {
encoder.encodeString(value.string)
encoder.encodeByte(0)
}
...
override fun encodeString(value: String) {
output.writeString(value)
}
override fun encodeByte(value: Byte) {
output.writeByte(value)
}
Zyle Moore
12/01/2024, 6:46 PMZyle Moore
12/07/2024, 11:18 PMEncoder
interface actually has has a method encodeSerializableValue
which delegates encoding of that value to a different Serializer.Pavel Kazlovich
06/25/2025, 7:49 AMZyle Moore
06/25/2025, 11:31 PMAbstractDecoder
, I didn't get access to the decodeStringElement
, just decodeString
. But when it came to polymorphic serialization, I also couldn't control how it determined polymorphism. The PolymorphicSerializer
will read element 0
using decodeStringElement
, and then use that string value to look up the actual serializer to use.Zyle Moore
06/25/2025, 11:33 PMAbstractDecoder
and implement both Decoder, CompositeDecoder
.Zyle Moore
06/25/2025, 11:36 PMZyle Moore
06/25/2025, 11:38 PMZyle Moore
06/26/2025, 12:26 AMencode*
call.Zyle Moore
06/26/2025, 12:30 AMencode*
method. This was similar to some of the other known encoders, like Json, since they have encodeJsonElement
methods.Zyle Moore
06/26/2025, 12:31 AMencodeTypeTag
, encodeFieldValue
, encodeRecordSize
, encodeGroupFlags
, etc.Zyle Moore
06/26/2025, 12:38 AMConsoleEncoder
or PrintEncoder
could react to encodeInt
by calling println(intValue)
. There was some kind of natural way of dealing with the requests. With the Kotlinx IO Buffer I used, there were writeIntLe
and readDouble
I could use to react to those encode methods. There was external state that persisted between calls. Another example someone gave me was a ListEncoder
, which would go encodeInt => list.add(intValue)
. This became more apparent when I saw that the format classes in the existing serializers created state objects (JsonStringWriter
) which were used to create the encoders (like JsonEncoder(JsonStringWriter())
).Zyle Moore
06/26/2025, 12:42 AMZyle Moore
06/26/2025, 12:49 AMencodeStructure, encodeIntElement, encodeStringElement, endStructure
. Or rather, the serializer will treat MyFormatType
and Pair<String, String>
the same. It doesn't know that one is for your format, and one is a standard type. They're both just serializable things.Zyle Moore
06/26/2025, 12:54 AMJsonSerializer.Serialize
was a thing I could do statically. And there was only ever really the one representation, the one derived automatically from the class. but Kotlin Serializers (and descriptors) are kind of more like HTTP Resources. What I mean is, HTTP Resources can have lots of different representations. POST /mypage
could Accept
HTML, or CSS, or Javascript, or JSON, or XML, or CSV, or PNG, or JPG. That's 7 or more representations of the /mypage
resource. These are kind of the same, in that, if you don't use the plugin generated Serializer, you have to manage that specific representation yourself.Zyle Moore
06/26/2025, 12:57 AMPoint2D(x, y)
class, and you're totally fine with the encoder using everything as-is (types, order of elements), you get that out of the box. If you wanted to normalize the x and y, or swap the order of the fields, or include additional metadata, or map it do a different type and provide a default, you'd have to do all of that in the serializer, because it's concerned with the number of elements, the types, and the order of elements. You can also have multiple Serializers for the same type, like BackwardsStringSerializer : KSerializer<String>
and UppercaseStringSerializer : KSerializer<String>
Zyle Moore
06/26/2025, 12:59 AMencodeInt
in the JsonEncoder effectively just calls encodeString
. There's nothing more specific you need to to, because someInt.toString()
is going to be the correct string for that value.Pavel Kazlovich
06/26/2025, 9:04 AM