Loading page…
Rust walkthroughs
Loading page…
serde, how does #[serde(tag = "type")] differ from #[serde(tag = "type", content = "data")] for enum serialization?#[serde(tag = "type")] uses internally tagged representation where the variant name appears as a field alongside the variant's data in a single JSON object. #[serde(tag = "type", content = "data")] uses adjacently tagged representation where the variant name and data are separate sibling fields. Internally tagged enums serialize to {"type": "Variant", ...other_fields} while adjacently tagged enums serialize to {"type": "Variant", "data": {...fields}}. The choice affects both serialization format and deserialization behavior: internally tagged enums must have variants with named fields (struct variants), while adjacently tagged enums can handle any variant type including unit and tuple variants. Internally tagged is cleaner for struct variants but cannot represent tuple or unit variants; adjacently tagged is more flexible but produces nested JSON.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
enum Message {
Request { id: u32, method: String },
Response { id: u32, result: String },
}
fn main() {
let msg = Message::Request {
id: 1,
method: "get".to_string()
};
let json = serde_json::to_string(&msg).unwrap();
println!("{}", json);
// {"Request":{"id":1,"method":"get"}}
}By default, enums use externally tagged representation with variant name as the outer key.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
#[serde(tag = "type")]
enum Message {
Request { id: u32, method: String },
Response { id: u32, result: String },
}
fn main() {
let msg = Message::Request {
id: 1,
method: "get".to_string()
};
let json = serde_json::to_string(&msg).unwrap();
println!("{}", json);
// {"type":"Request","id":1,"method":"get"}
}tag = "type" puts the variant name as a field alongside other fields.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
#[serde(tag = "type", content = "data")]
enum Message {
Request { id: u32, method: String },
Response { id: u32, result: String },
}
fn main() {
let msg = Message::Request {
id: 1,
method: "get".to_string()
};
let json = serde_json::to_string(&msg).unwrap();
println!("{}", json);
// {"type":"Request","data":{"id":1,"method":"get"}}
}tag and content separate variant name and data into sibling fields.
use serde::{Serialize, Deserialize};
// This compiles but won't work for all variants
#[derive(Serialize, Deserialize)]
#[serde(tag = "type")]
enum Status {
Active, // Unit variant - ERROR at runtime
Pending(String), // Tuple variant - ERROR at runtime
Completed { success: bool }, // Struct variant - OK
}
// Internally tagged only works with struct variants
// Unit and tuple variants cause serialization errorsInternally tagged enums cannot have unit or tuple variants.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
#[serde(tag = "type", content = "data")]
enum Status {
Active, // Unit variant - OK
Pending(String), // Tuple variant - OK
Completed { success: bool }, // Struct variant - OK
}
fn main() {
let active = Status::Active;
println!("{}", serde_json::to_string(&active).unwrap());
// {"type":"Active"}
let pending = Status::Pending("waiting".to_string());
println!("{}", serde_json::to_string(&pending).unwrap());
// {"type":"Pending","data":"waiting"}
let completed = Status::Completed { success: true };
println!("{}", serde_json::to_string(&completed).unwrap());
// {"type":"Completed","data":{"success":true}}
}Adjacently tagged handles unit, tuple, and struct variants.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type")]
enum Internal {
Start { name: String },
Stop { reason: String },
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type", content = "data")]
enum Adjacent {
Start { name: String },
Stop { reason: String },
}
fn main() -> Result<(), serde_json::Error> {
// Internally tagged: fields at same level as tag
let internal_json = r#"{"type":"Start","name":"process"}"#;
let internal: Internal = serde_json::from_str(internal_json)?;
println!("{:?}", internal);
// Adjacently tagged: fields nested under content key
let adjacent_json = r#"{"type":"Start","data":{"name":"process"}}"#;
let adjacent: Adjacent = serde_json::from_str(adjacent_json)?;
println!("{:?}", adjacent);
// Each format requires matching JSON structure
// Can't deserialize internal format into adjacent enum
Ok(())
}Deserialization requires JSON matching the tag format.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
#[serde(tag = "type", content = "data")]
enum Event {
Click { x: i32, y: i32 },
KeyPress { key: String, modifiers: Vec<String> },
Scroll { direction: String, amount: u32 },
}
fn main() {
let click = Event::Click { x: 100, y: 200 };
println!("{}", serde_json::to_string_pretty(&click).unwrap());
// {
// "type": "Click",
// "data": {
// "x": 100,
// "y": 200
// }
// }
let keypress = Event::KeyPress {
key: "Enter".to_string(),
modifiers: vec!["Shift".to_string()]
};
println!("{}", serde_json::to_string_pretty(&keypress).unwrap());
// {
// "type": "KeyPress",
// "data": {
// "key": "Enter",
// "modifiers": ["Shift"]
// }
// }
}The content field wraps all variant data as a nested object.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
#[serde(tag = "type")]
enum Message {
// If a variant has a field named "type", it conflicts
Request {
#[serde(rename = "msgType")] // Rename to avoid conflict
type: String, // Would conflict with tag
id: u32
},
}
// The tag field name cannot be used by any variant field
// Use rename on the conflicting fieldTag field name must not conflict with any variant field names.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
#[serde(tag = "type", content = "data")]
enum Outer {
Inner(Inner),
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "type")]
enum Inner {
A { value: i32 },
B { value: String },
}
fn main() {
let outer = Outer::Inner(Inner::A { value: 42 });
println!("{}", serde_json::to_string(&outer).unwrap());
// {"type":"Inner","data":{"type":"A","value":42}}
}Nested enums combine their tagging formats.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
#[serde(tag = "kind", content = "payload")]
enum Message {
Ping,
Pong { timestamp: u64 },
}
fn main() {
let ping = Message::Ping;
println!("{}", serde_json::to_string(&ping).unwrap());
// {"kind":"Ping"}
let pong = Message::Pong { timestamp: 12345 };
println!("{}", serde_json::to_string(&pong).unwrap());
// {"kind":"Pong","payload":{"timestamp":12345}}
}Choose field names that fit your JSON schema.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum Value {
Int(i32),
String(String),
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "type", content = "data")]
enum TaggedValue {
Int(i32),
String(String),
}
fn main() {
// Untagged: no wrapper, inferred from content
let untagged_int = Value::Int(42);
println!("{}", serde_json::to_string(&untagged_int).unwrap());
// 42
// Tagged: explicit type field
let tagged_int = TaggedValue::Int(42);
println!("{}", serde_json::to_string(&tagged_int).unwrap());
// {"type":"Int","data":42}
}Untagged enums have no type field; content is serialized directly.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
#[serde(tag = "type", content = "data")]
enum Status {
#[serde(skip_serializing)]
Unknown, // Unit variant
Ready,
Error(String),
}
fn main() {
// Unit variants with adjacent tagging: data field is null or absent
let ready = Status::Ready;
println!("{}", serde_json::to_string(&ready).unwrap());
// {"type":"Ready"}
}Unit variants with adjacent tagging don't include a content field.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
#[serde(tag = "type", content = "data")]
enum Tuple {
Point(i32, i32),
Named { x: i32, y: i32 },
}
fn main() {
let point = Tuple::Point(10, 20);
println!("{}", serde_json::to_string(&point).unwrap());
// {"type":"Point","data":[10,20]}
let named = Tuple::Named { x: 10, y: 20 };
println!("{}", serde_json::to_string(&named).unwrap());
// {"type":"Named","data":{"x":10,"y":20}}
}Tuple variants serialize as arrays, struct variants as objects.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type")]
enum Known {
A { value: i32 },
B { value: String },
}
fn main() {
// Unknown variant causes error
let json = r#"{"type":"C","value":123}"#;
let result: Result<Known, _> = serde_json::from_str(json);
match result {
Ok(known) => println!("{:?}", known),
Err(e) => println!("Error: {}", e),
// Error: unknown variant `C`, expected `A` or `B`
}
}Unknown variants are rejected during deserialization.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
#[serde(tag = "type")]
enum Message {
Request { id: u32 },
Response { id: u32 },
}
// Use untagged enum as fallback
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum AnyMessage {
Known(Message),
Unknown(serde_json::Value),
}
fn main() {
let known = r#"{"type":"Request","id":1}"#;
let msg: AnyMessage = serde_json::from_str(known).unwrap();
println!("{:?}", msg);
let unknown = r#"{"type":"Unknown","data":"something"}"#;
let msg: AnyMessage = serde_json::from_str(unknown).unwrap();
println!("{:?}", msg);
}Combine tagged with untagged for flexible parsing.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
#[serde(tag = "status", content = "data")]
#[serde(rename_all = "snake_case")]
enum ApiResponse<T> {
Success(T),
Error { code: u32, message: String },
}
#[derive(Serialize, Deserialize)]
struct User {
id: u32,
name: String,
}
fn main() {
let success = ApiResponse::Success(User {
id: 1,
name: "Alice".to_string()
});
println!("{}", serde_json::to_string_pretty(&success).unwrap());
// {
// "status": "success",
// "data": {
// "id": 1,
// "name": "Alice"
// }
// }
let error = ApiResponse::Error::<User> {
code: 404,
message: "Not found".to_string()
};
println!("{}", serde_json::to_string_pretty(&error).unwrap());
// {
// "status": "error",
// "data": {
// "code": 404,
// "message": "Not found"
// }
// }
}Adjacently tagged enums work well for generic result types.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct Metadata {
timestamp: u64,
source: String,
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "event_type")]
enum Event {
#[serde(rename = "click")]
Click {
#[serde(flatten)]
meta: Metadata,
x: i32,
y: i32,
},
#[serde(rename = "scroll")]
Scroll {
#[serde(flatten)]
meta: Metadata,
direction: String,
},
}
fn main() {
let event = Event::Click {
meta: Metadata {
timestamp: 12345,
source: "web".to_string()
},
x: 100,
y: 200,
};
println!("{}", serde_json::to_string_pretty(&event).unwrap());
// {
// "event_type": "click",
// "timestamp": 12345,
// "source": "web",
// "x": 100,
// "y": 200
// }
}Flatten embeds struct fields at the same level as the tag.
| Feature | Externally Tagged (default) | Internally Tagged | Adjacently Tagged |
|---------|---------------------------|-------------------|-------------------|
| JSON format | {"Variant": {...}} | {"type":"Variant",...} | {"type":"Variant","data":{...}} |
| Tag attribute | None | tag = "field" | tag = "field", content = "data" |
| Unit variants | ✓ | ✗ | ✓ |
| Tuple variants | ✓ | ✗ | ✓ |
| Struct variants | ✓ | ✓ | ✓ |
| Flat structure | No | Yes | No (nested) |
| Use case | Simple enums | Struct variants only | Any variant type |
The choice between tag and tag + content depends on your data format requirements:
Internally tagged (tag = "type") produces cleaner JSON for struct variants—fields appear at the same level as the type field. This matches common JSON API conventions where type discrimination happens via a type field. However, it only works with struct variants; unit and tuple variants will cause runtime errors. Use internally tagged when all variants have named fields and you want flat JSON.
Adjacently tagged (tag = "type", content = "data") separates the variant name from variant data into two sibling fields. This works with all variant types including unit and tuple variants. The data is nested under the content field. Use adjacently tagged when you need tuple or unit variants, or when your JSON schema requires the variant data to be nested separately.
Key insight: The tag attribute fundamentally changes enum representation from the default externally tagged format. Externally tagged wraps content in the variant name as key; internally tagged embeds the variant name as a field alongside content; adjacently tagged uses two fields for variant name and content. Choose based on your JSON schema requirements and variant types. For struct-only enums matching common JSON patterns, internally tagged is cleaner. For enums with mixed variant types, adjacently tagged is necessary.