Enum representations

Consider the following enum type:

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

Externally tagged

The default representation for this enum in Serde is called the externally tagged enum representation. Written in JSON syntax it looks like:

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

The externally tagged representation is characterized by being able to know which variant we are dealing with before beginning to parse the content of the variant. This property allows it to work across a broad range of text and binary formats. The Serializer::serialize_*_variant and Deserializer::deserialize_enum methods use an externally tagged representation.

This representation can handle any type of variant: struct variants like above, tuple variants, newtype variants, and unit variants.

In JSON and other self-describing formats, the externally tagged representation is often not ideal for readability. Serde provides attributes to select two other possible representations.

Internally tagged

#[derive(Serialize, Deserialize)]
#[serde(tag = "type")]
enum Message {
    Request { id: String, method: String, params: Params },
    Response { id: String, result: Value },
}

Written in JSON syntax, the internally tagged representation looks like this:

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

The tag identifying which variant we are dealing with is now inside of the content, next to any other fields of the variant. This representation is common in Java libraries.

This representation works for struct variants, newtype variants containing structs or maps, and unit variants but does not work for enums containing tuple variants. Using a #[serde(tag = "...")] attribute on an enum containing a tuple variant is an error at compile time.

Adjacently tagged

#[derive(Serialize, Deserialize)]
#[serde(tag = "t", content = "c")]
enum Block {
    Para(Vec<Inline>),
    Str(String),
}

This representation is common the Haskell world. Written in JSON syntax:

{"t": "Para", "c": [{...}, {...}]}
{"t": "Str", "c": "the string"}

The tag and the content are adjacent to each other as two fields within the same object.

Untagged

#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum Message {
    Request { id: String, method: String, params: Params },
    Response { id: String, result: Value },
}

Written in JSON syntax, the untagged representation looks like this:

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

There is no explicit tag identifying which variant the data contains. Serde will try to match the data against each variant in order and the first one that deserializes successfully is the one returned.

This representation can handle enums containing any type of variant.

As another example of an untagged enum, this enum can be deserialized from either an integer or an array of two strings:

#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum Data {
    Integer(u64),
    Pair(String, String),
}