LengthFrom

annotation class LengthFrom(val field: String)

Marks a field whose byte length is determined by a numeric sibling elsewhere in the message — the length is carried as a separate constructor parameter because the consumer cares about it as a number (flow control, routing) or because the on-wire prefix shape is one @LengthPrefixed cannot express (e.g. TLS uint24).

Accepted on:

  • String fields — body is a single UTF-8 string sized by the sibling.

  • List<T> where T is a @ProtocolMessage data class — body is a sequence of nested-message bodies, byte-bounded by the sibling.

  • T where T is a @ProtocolMessage data class or sealed parent — body is a single nested message; the sibling-derived length covers the whole nested wire form. Decode narrows buffer.limit() to the bounded extent and delegates to <TCodec>.decode; encode delegates to <TCodec>.encode and the user is responsible for sizing the sibling to the body's wire byte count.

When to prefer @LengthPrefixed

For adjacent length carriers, prefer @LengthPrefixed when the prefix shape matches one of LengthPrefix.Byte / LengthPrefix.Short / LengthPrefix.Int. A field whose only purpose is to bound the immediately following sibling is a redundant length carrier — modeling it as an independent constructor parameter encodes the same quantity twice (prefix vs. value's wireSize). The validator rejects the redundant shape for String and List<T> bodies.

When @LengthFrom is the only option

Adjacent siblings remain valid for nested @ProtocolMessage bodies because @LengthPrefixed only supports 1 / 2 / 4-byte prefixes — protocols with non-standard prefix widths cannot express their wire shape via @LengthPrefixed. Example: TLS 1.3 handshake header (RFC 8446 §4) uses a 3-byte big-endian length:

@ProtocolMessage(wireOrder = Endianness.Big)
data class TlsHandshake(
val msgType: UByte,
@WireBytes(3) val length: UInt, // uint24
@LengthFrom("length") val body: TlsHandshakeBody,
)

Genuine remote-prefix: length carried in a non-adjacent field, often parsed by a different codec or sitting several positions away (MQTT-style header-bounded payloads, parent-passed bounds via @DispatchOn, etc.):

@ProtocolMessage
data class RemoteHeader(
val payloadLength: UShort, // consumer-visible — flow control, routing
val flags: UByte,
val correlationId: UInt,
@LengthFrom("payloadLength") val payload: String,
)

Parameters

field

The name of the sibling field that holds the byte length. Must exist, come before this field, and resolve to either a non-nullable numeric scalar (Byte/Short/Int/Long/UByte/ UShort/UInt/ULong) for the simple form "siblingName", or a value class with a val property returning non-nullable Int for the dotted form "siblingName.property".

Properties

Link copied to clipboard