Module musli::derives

source ·
Expand description

§Documentation for deriving Encode and Decode

The Encode and Decode derives allows for automatically implementing Encode and Decode.

They come with a number of options for customizing their implementation, detailed below. But first we need to talk about modes.


§Attributes
  • Meta attributes which apply to the attribute itself. It is used to filter what scope the current attribute applies to, such as only applying to an Encode derive using #[musli(encode_only, ..)] or a specific mode such as #[musli(mode = Json, ..)].

  • Container attributes are attributes which apply to the struct or enum.

  • Variant attributes are attributes which apply to each individual variant in an enum.

  • Field attributes are attributes which apply to each individual field either in a struct or an enum variant.


§Modes

If you’ve paid close attention to the Encode and Decode traits you might’ve noticed that they have an extra parameter called M which stands for “mode”.

This allows a single type to have more than one implementation of encoding traits, allowing for a high level of flexibility in how a type should be encoded.

When it comes to deriving these traits you can scope attributes to apply to either any mode, the default mode, or a completely custom mode. This is done using the #[musli(mode = ..)] meta attribute like this:

use musli::{Encode, Decode};

enum Json {}

#[derive(Encode, Decode)]
#[musli(name_all = "index")]
#[musli(mode = Json, name_all = "name")]
struct Person<'a> {
    name: &'a str,
    age: u32,
}

What this means is that if we want to serialize Person using named fields, we can simply turn on the Json mode for our given serializer. If we want to revert back to the default behavior and use indexed fields we can instead use Binary.

use musli_json::Encoding;

const JSON_ENCODING: Encoding<Json> = Encoding::new().with_mode();
const DEFAULT_ENCODING: Encoding = Encoding::new();

let named = JSON_ENCODING.to_vec(&Person { name: "Aristotle", age: 62 })?;
assert_eq!(named.as_slice(), b"{\"name\":\"Aristotle\",\"age\":62}");

let indexed = DEFAULT_ENCODING.to_vec(&Person { name: "Plato", age: 84 })?;
assert_eq!(indexed.as_slice(), b"{\"0\":\"Plato\",\"1\":84}");

So the #[musli(mode)] atttribute is supported in any position. And any of its sibling attributes will be added to the given alternative mode, rather the default mode.


§Meta attributes

Certain attributes affect which other attributes apply to a given context. These are called meta attributes.

Meta attributes are applicable to any context, and can be used on containers, variants, and fields.


§#[musli(mode = <path>)]

The attributes only apply to the given mode.

The Person struct below uses string field names when the Json mode is enabled, otherwise it uses default numerical field names.

use musli::{Encode, Decode};

enum Json {}

#[derive(Encode, Decode)]
#[musli(mode = Json, name_all = "name")]
struct Person<'a> {
    name: &'a str,
    age: u32,
}

§#[musli(encode_only)]

The attributes only apply when implementing the Encode trait.

An example where this is useful is if you want to apply #[musli(packed)] in a different mode, but only for encoding, since decoding packed types is not supported for enums.

use musli::mode::Binary;
use musli::{Decode, Encode};

enum Packed {}

#[derive(Encode, Decode)]
#[musli(mode = Packed, encode_only, packed)]
enum Name<'a> {
    Full(&'a str),
    Given(&'a str),
}

§#[musli(decode_only)]

The attributes only apply when implementing the Decode trait.

use musli::{Decode, Encode};

#[derive(Encode, Decode)]
#[musli(name_all = "name")]
struct Name<'a> {
    sur_name: &'a str,
    #[musli(decode_only, name = "last")]
    last_name: &'a str,
}

§Container attributes

Container attributes apply to the container, such as directly on the struct or enum. Like the uses of #[musli(packed)] and #[musli(name_all = "name")] here:

use musli::{Encode, Decode};

#[derive(Encode, Decode)]
#[musli(packed)]
struct Struct {
    /* the body of the struct */
}

#[derive(Encode, Decode)]
#[musli(name_all = "name")]
enum Enum {
    /* the body of the enum */
}

§#[musli(name_all = "..")]

Allos for renaming every field in the container. It can take any of the following values:

  • index (default) - the index of the field will be used.
  • name - the literal name of the field will be used.
  • PascalCase - the field will be converted to pascal case.
  • camelCase - the field will be converted to camel case.
  • snake_case - the field will be converted to snake case.
  • SCREAMING_SNAKE_CASE - the field will be converted to screaming snake case.
  • kebab-case - the field will be converted to kebab case.
  • SCREAMING-KEBAB-CASE - the field will be converted to screaming kebab case.
use musli::{Encode, Decode};

#[derive(Encode, Decode)]
#[musli(name_all = "PascalCase")]
struct PascalCaseStruct {
    field_name: u32,
}

#[derive(Encode, Decode)]
#[musli(name_all = "name")]
struct NamedStruct {
    field1: u32,
    field2: u32,
}

If applied to an enum, it will instead rename all variants:

use musli::{Encode, Decode};

#[derive(Encode, Decode)]
#[musli(name_all = "PascalCase")]
enum PascalCaseEnum {
    VariantName {
        field_name: u32,
    }
}

#[derive(Encode, Decode)]
#[musli(name_all = "name")]
enum NamedEnum {
    Variant1 {
        field1: u32,
    },
    Variant2 {
        field1: u32,
    },
}

§#[musli(transparent)]

This can only be used on types which have a single field. It will cause that field to define how that variant is encoded or decoded transparently without being treated as a field.

use musli::{Encode, Decode};
use musli_wire::tag::{Tag, Kind};

#[derive(Encode)]
#[musli(transparent)]
struct Struct(u32);

let data = musli_wire::to_vec(&Struct(42))?;
assert_eq!(data.as_slice(), vec![Tag::new(Kind::Continuation, 42).byte()]);

§#[musli(packed)]

This attribute will disable all tagging and the structure will simply be encoded with one field following another in the order in which they are defined.

A caveat of packed structures is that they cannot be safely versioned and the two systems communicating through them need to be using strictly synchronized representations.

This attribute is useful for performing simple decoding over “raw” bytes when combined with an encoder which does minimal prefixing and packs fields.

use musli::{Encode, Decode};

#[derive(Encode)]
#[musli(packed)]
struct Struct {
    field1: u32,
    field2: u32,
    field3: u32,
}

let data = musli_storage::to_vec(&Struct {
    field1: 1,
    field2: 2,
    field3: 3,
})?;

assert_eq!(data.as_slice(), vec![1, 2, 3]);

§#[musli(name_type = ..)]

This indicates which type any contained #[musli(name = ..)] attributes should have. Tags can usually be inferred, but specifying this field ensures that all tags have a single well-defined type.

The following values are treated specially:

  • str applies #[musli(name_all = "name")] by default.
  • [u8] applies #[musli(name_all = "name")] by default.
use core::fmt;

use musli::{Encode, Decode};

#[derive(Debug, PartialEq, Eq, Encode, Decode)]
#[musli(transparent)]
struct CustomTag<'a>(&'a [u8]);

impl fmt::Display for CustomTag<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt::Debug::fmt(self.0, f)
    }
}

#[derive(Encode, Decode)]
#[musli(name_type = CustomTag)]
struct Struct {
    #[musli(name = CustomTag(b"name in bytes"))]
    name: String,
}

#[derive(Encode, Decode)]
#[musli(name_type = CustomTag)]
enum EnumWithCustomTag {
    #[musli(name = CustomTag(b"variant one"))]
    Variant1 {
        /* .. */
    },
}

§#[musli(name_method = ..)]

This allows for explicitly setting which method should be used to decode names. Available options are:

  • "value" (default) - the name is decoded as a value.
  • "unsized" - the name is decoded as an unsized value, this is the default if for example #[musli(name_type = str)] is used.
  • "unsized_bytes" - the name is decoded as a unsized bytes, this is the default if for example #[musli(name_type = [u8])] is used.

This can be overrided for values which are unsized, but cannot be determined through heuristics. Such a type must also implement Decode (for "value"), DecodeUnsized, or DecodeUnsizedBytes as appropriate.


§#[musli(bound = {..})] and #[musli(decode_bound = {..})]

These attributes can be used to apply bounds to an Encode or Decode implementation.

These are necessary to use when a generic container is used to ensure that the given parameter implements the necessary bounds.

#[musli(bound = {..})] applies to all implementations while #[musli(decode_bound = {..})] only applies to the Decode implementation. The latter allows for using the decode lifetime parameter (which defaults to 'de).

use musli::{Decode, Encode};

#[derive(Clone, Debug, PartialEq, Encode, Decode)]
#[musli(bound = {T: Encode<M>}, decode_bound = {T: Decode<'de, M>})]
pub struct GenericWithBound<T> {
    value: T,
}

§Enum attributes


§#[musli(tag = ..)]

This attribute causes the enum to be internally tagged, with the given tag. See enum representations for details on this representation.

#[derive(Encode, Decode)]
#[musli(name_all = "name", tag = "type")]
enum Message {
    Request { id: String, method: String, params: Params },
    Response { id: String, result: Value },
}

§Variant attributes

Variant attributes are attributes which apply to each individual variant in an enum. Like the use of #[musli(name = ..)] here:

use musli::{Encode, Decode};

#[derive(Encode, Decode)]
#[musli(name_all = "name")]
enum Enum {
    Variant {
        /* variant body */
    },
    #[musli(name = "Other")]
    Something {
        /* variant body */
    },
}

§#[musli(name = ..)]

This allows for renaming a variant from its default value. It can take any value (including complex ones) that can be serialized with the current encoding, such as:

  • #[musli(name = 1)]
  • #[musli(name = "Hello World")]
  • #[musli(name = b"box\0")]
  • #[musli(name = SomeStruct { field: 42 })] (if SomeStruct implements Encode and Decode as appropriate).

If the type of the tag is ambiguous it can be explicitly specified through the #[musli(name_type)] attribute.


§#[musli(pattern = ..)]

A pattern to match for decoding a variant.

This allows for more flexibility when decoding variants.

use musli::{Encode, Decode};

#[derive(Encode, Decode)]
enum Enum {
    Variant1,
    Variant2,
    #[musli(pattern = 2..=4)]
    Deprecated,
}

§#[musli(name_all = "..")]

Allos for renaming every field in the variant. It can take any of the following values:

  • index (default) - the index of the field will be used.
  • name - the literal name of the field will be used.
  • PascalCase - the field will be converted to pascal case.
  • camelCase - the field will be converted to camel case.
  • snake_case - the field will be converted to snake case.
  • SCREAMING_SNAKE_CASE - the field will be converted to screaming snake case.
  • kebab-case - the field will be converted to kebab case.
  • SCREAMING-KEBAB-CASE - the field will be converted to screaming kebab case.
use musli::{Encode, Decode};

#[derive(Encode, Decode)]
enum PascalCaseEnum {
    #[musli(name_all = "PascalCase")]
    Variant {
        field_name: u32,
    }
}

#[derive(Encode, Decode)]
enum NamedEnum {
    #[musli(name_all = "name")]
    Variant {
        field1: u32,
    },
    Variant2 {
        field1: u32,
    },
}

§#[musli(name_type = ..)]

This indicates which type any contained #[musli(tag = ..)] attributes should have. Tags can usually be inferred, but specifying this field ensures that all tags have a well-defined type.

The following values are treated specially:

  • str applies #[musli(name_all = "name")] by default.
  • [u8] applies #[musli(name_all = "name")] by default.
use core::fmt;

use musli::{Encode, Decode};

#[derive(Debug, PartialEq, Eq, Encode, Decode)]
#[musli(transparent)]
struct CustomTag<'a>(&'a [u8]);

impl fmt::Display for CustomTag<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt::Debug::fmt(self.0, f)
    }
}

#[derive(Encode, Decode)]
#[musli(name_type = usize)]
enum Enum {
    #[musli(name = 0usize, name_type = CustomTag)]
    Variant {
        #[musli(name = CustomTag(b"field1"))]
        field1: u32,
        #[musli(name = CustomTag(b"field2"))]
        field2: u32,
    },
    #[musli(name = 1usize, name_all = "name")]
    Variant2 {
        #[musli(name = "field1")]
        field1: u32,
        #[musli(name = "field2")]
        field2: u32,
    },
}

§#[musli(name_method = ..)]

This allows for explicitly setting which method should be used to decode field names. Available options are:

  • "value" (default) - the name is decoded as a value.
  • "unsized" - the name is decoded as an unsized value, this is the default if for example #[musli(name_type = str)] is used.
  • "unsized_bytes" - the name is decoded as a unsized bytes, this is the default if for example #[musli(name_type = [u8])] is used.

This can be overrided for values which are unsized, but cannot be determined through heuristics. Such a type must also implement Decode (for "value"), DecodeUnsized, or DecodeUnsizedBytes as appropriate.


§#[musli(transparent)]

This can only be used on variants which have a single field. It will cause that field to define how that variant is encoded or decoded transparently without being treated as a field.


§#[musli(default)]

This defines the variant that will be used in case no other variant matches. Only one such variant can be defined.

use musli::{Encode, Decode};

#[derive(Debug, PartialEq, Eq, Encode, Decode)]
#[musli(name_all = "kebab-case")]
enum Animal {
    Cat,
    Dog,
    #[musli(default)]
    Unknown,
}

§Field attributes

Field attributes are attributes which apply to each individual field either in a struct or an enum variant. Like the uses of #[musli(all)] here:

use musli::{Encode, Decode};

#[derive(Encode, Decode)]
#[musli(name_all = "name")]
struct Struct {
    #[musli(name = "other")]
    something: String,
    #[musli(skip, default = default_field)]
    skipped_field: u32,
}

fn default_field() -> u32 {
    42
}

#[derive(Encode, Decode)]
#[musli(name_all = "name")]
enum Enum {
    #[musli(name_all = "name")]
    Variant {
        #[musli(name = "other")]
        something: String,
    }
}

§#[musli(skip)]

This attribute means that the entire field is skipped. If a field is decoded it uses Default::default to construct the value. Other defaults can be specified with [#[musli(default = <path>)]][#muslidefault–path].

use musli::{Encode, Decode};

#[derive(Encode, Decode)]
struct Person {
    name: String,
    #[musli(skip)]
    age: Option<u32>,
    #[musli(skip, default = default_country)]
    country: Option<String>,
}

fn default_country() -> Option<String> {
    Some(String::from("Earth"))
}

§#[musli(default [= <path>])]

When a field is absent or disabled with #[musli(skip)], this attribute specifies that a default value should be used instead.

If #[musli(default)] is specified, the default value is constructed using Default::default.

If #[musli(default = <path>)] is specified, the default value is constructed by calling the function at <path>.

use musli::{Encode, Decode};

#[derive(Encode, Decode)]
struct Person {
    name: String,
    #[musli(default)]
    age: Option<u32>,
    #[musli(default = default_height)]
    height: Option<u32>,
    #[musli(skip, default = default_meaning)]
    meaning: u32,
}

fn default_height() -> Option<u32> {
    Some(180)
}

fn default_meaning() -> u32 {
    42
}

§#[musli(name = ..)]

This allows for renaming a field from its default value. It can take any value (including complex ones) that can be serialized with the current encoding, such as:

  • #[musli(name = 1)]
  • #[musli(name = "Hello World")]
  • #[musli(name = b"box\0")]
  • #[musli(name = SomeStruct { field: 42 })] (if SomeStruct implements Encode and Decode as appropriate).

If the type of the tag is ambiguous it can be explicitly specified through the #[musli(name_type)] variant or container attributes.


§#[musli(pattern = ..)]

A pattern to match for decoding the given field.

This allows for more flexibility when decoding fields.

use musli::{Encode, Decode};

#[derive(Encode, Decode)]
struct Struct {
    field1: u32,
    field2: u32,
    #[musli(pattern = 2..=4)]
    other: u32,
}

§#[musli(packed)]

This specifies that encoding and decoding should happen through the EncodePacked and DecodePacked traits, instead of the default Encode and Decode.

These traits contained implementations which are biased towards encoding the field as a compact, non-future compatible pack. In essense, the fields are encoded “one after another” without any metadata separating them. So for packed fields, the order, types and number of the fields are important.

use std::collections::VecDeque;

use musli::{Decode, Encode};

#[derive(Decode, Encode)]
struct Container {
    #[musli(packed)]
    tuple: (u32, u64),
    #[musli(packed)]
    array: [u32; 4],
}

§#[musli(bytes)]

This specifies that encoding and decoding should happen through the EncodeBytes and DecodeBytes traits, instead of the default Encode and Decode.

These traits contained implementations which are biased towards encoding the field as an array of bytes.

use std::collections::VecDeque;

use musli::{Decode, Encode};

#[derive(Decode, Encode)]
struct Container<'de> {
    #[musli(bytes)]
    vec: Vec<u8>,
    #[musli(bytes)]
    vec_deque: VecDeque<u8>,
    #[musli(bytes)]
    bytes: &'de [u8],
}

§#[musli(with = <path>)]

This specifies the path to a module to use instead of the fields default Encode or Decode implementations.

It expects encode and decode and decodee function to be defined in the path being specified, like this:

use musli::{Decode, Encode};

#[derive(Decode, Encode)]
struct Container {
    #[musli(with = self::module)]
    field: Field,
}

struct Field {
    /* internal */
}

mod module {
    use musli::{Decoder, Encoder};

    use super::Field;

    pub fn encode<E>(field: &Field, cx: &E::Cx, encoder: E) -> Result<E::Ok, E::Error>
    where
        E: Encoder,

    pub fn decode<'de, D>(cx: &D::Cx, decoder: D) -> Result<Field, D::Error>
    where
        D: Decoder<'de>,
}

This can also be generic such as:

use musli::{Decode, Encode};

#[derive(Decode, Encode)]
struct Container {
    #[musli(with = self::module)]
    field: Field<u32>,
}

struct Field<T> {
    /* internal */
}

mod module {
    use musli::{Decoder, Encoder};

    use super::Field;

    pub fn encode<E, T>(field: &Field<T>, cx: &E::Cx, encoder: E) -> Result<E::Ok, E::Error>
    where
        E: Encoder,

    pub fn decode<'de, D, T>(cx: &D::Cx, decoder: D) -> Result<Field<T>, D::Error>
    where
        D: Decoder<'de>,
}

More complete example:

use std::collections::HashSet;
use musli::{Encode, Decode};

pub struct CustomUuid(u128);

#[derive(Encode, Decode)]
struct Struct {
    #[musli(with = self::custom_uuid)]
    id: CustomUuid,
    #[musli(with = self::custom_set)]
    numbers: HashSet<u32>,
}

mod custom_uuid {
    use musli::{Context, Decode, Decoder, Encode, Encoder};

    use super::CustomUuid;

    pub fn encode<E>(uuid: &CustomUuid, cx: &E::Cx, encoder: E) -> Result<E::Ok, E::Error>
    where
        E: Encoder,
    {
        uuid.0.encode(cx, encoder)
    }

    pub fn decode<'de, D>(cx: &D::Cx, decoder: D) -> Result<CustomUuid, D::Error>
    where
        D: Decoder<'de>,
    {
        Ok(CustomUuid(decoder.decode()?))
    }
}

mod custom_set {
    use std::collections::HashSet;
    use std::hash::Hash;

    use musli::{Context, Decode, Decoder, Encode, Encoder};

    pub fn encode<E, T>(set: &HashSet<T>, cx: &E::Cx, encoder: E) -> Result<E::Ok, E::Error>
    where
        E: Encoder,
        T: Encode<E::Mode> + Eq + Hash,
    {
        encoder.encode(set)
    }

    pub fn decode<'de, D, T>(cx: &D::Cx, decoder: D) -> Result<HashSet<T>, D::Error>
    where
        D: Decoder<'de>,
        T: Decode<'de, D::Mode> + Eq + Hash,
    {
        decoder.decode()
    }
}

§#[musli(skip_encoding_if = <path>)]

This adds a condition to skip encoding a field entirely if the condition is true. This is very commonly used to skip over encoding Option<T> fields.

use musli::{Encode, Decode};

#[derive(Encode, Decode)]
struct Person {
    name: String,
    #[musli(skip_encoding_if = Option::is_none)]
    age: Option<u32>,
}
§#[musli(trace)]

This causes the field to use the DecodeTrace / EncodeTrace when encoding the field. This is left optional for types where enabling tracing for the field requires extra traits to be implemented, such as HashMap<K, V> where we’d need K to implement fmt::Display.

Without using the trace attribute below, the keys in the values field would not be instrumented, so with a decoding error you’d see this:

.values: not numeric (at bytes 15-16)

Instead of this (where #[musli(trace)] is enabled):

.values[Hello]: not numeric (at bytes 15-16)
use std::collections::HashMap;

use musli::{Encode, Decode};

#[derive(Encode, Decode)]
struct Collection {
    #[musli(trace)]
    values: HashMap<String, u32>,
}

§Enum representations

Müsli supports the following enum representations, which mimics the ones supported by serde:

  • Externally tagged (default).
  • Internally tagged when #[musli(tag = ..)] is specified on the enum.
  • Adjacently tagged when both #[musli(tag = ..)] and #[musli(content)] are specified.

§Externally tagged

#[derive(Encode, Decode)]
enum Message {
    Request { id: String, method: String, params: Params },
    Response { id: String, result: Value },
}

When an enum is externally tagged it is represented by a single field indicating the variant of the enum.

{"Request": {"id": "...", "method": "...", "params": {...}}}

This is the most portable representation and is supported by most formats. It has special support in the Encoder and Decoder traits through Encoder::encode_variant and Decoder::decode_variant.

Conceptually this can be considered as a “pair”, where the variant tag can be extracted from the format before the variant is decoded.


§Internally tagged

#[derive(Encode, Decode)]
#[musli(name_all = "name", tag = "type")]
enum Message {
    Request { id: String, method: String, params: Params },
    Response { id: String, result: Value },
}

In JSON, the Message::Request would be represented as:

{"type": "Request", "id": "...", "method": "...", "params": {...}}

This is only supported by formats which are self descriptive, which is a requirement for the format to be buffered through Decoder::decode_buffer.

It is necessary to buffer the value, since we need to inspect the fields of a map for the field corresponding to the tag, and then use this to determine which decoder implementation to call.