Loading pageā¦
Rust walkthroughs
Loading pageā¦
serde's #[serde(tag = "type")] enable internally tagged enums for serialization?#[serde(tag = "type")] configures an enum to serialize with a type discriminator field embedded inside the variant's data structure rather than wrapping it externally. This creates a flatter JSON representation where the tag field appears alongside variant data fields, matching common API designs that use a "type" field for variant identification. Without tag, serde uses externally tagged representation where the variant name becomes a key wrapping the data. Internally tagged enums work best when all variants share a similar structure or when integrating with APIs that follow this discriminator pattern, but they require all variants to be structs with named fields and cannot represent unit or newtype variants in the same way.
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
enum Event {
Click { x: i32, y: i32 },
KeyPress { key: String },
MouseMove { x: i32, y: i32 },
}
fn main() {
let event = Event::Click { x: 100, y: 200 };
let json = serde_json::to_string(&event).unwrap();
println!("{}", json);
// {"Click":{"x":100,"y":200}}
let key_event = Event::KeyPress { key: "Enter".to_string() };
let json = serde_json::to_string(&key_event).unwrap();
println!("{}", json);
// {"KeyPress":{"key":"Enter"}}
}By default, serde uses external tagging: the variant name wraps the variant data.
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
enum Event {
Click { x: i32, y: i32 },
KeyPress { key: String },
MouseMove { x: i32, y: i32 },
}
fn main() {
let event = Event::Click { x: 100, y: 200 };
let json = serde_json::to_string(&event).unwrap();
println!("{}", json);
// {"type":"Click","x":100,"y":200}
let key_event = Event::KeyPress { key: "Enter".to_string() };
let json = serde_json::to_string(&key_event).unwrap();
println!("{}", json);
// {"type":"KeyPress","key":"Enter"}
}With tag = "type", the variant name becomes a field inside the object.
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
Triangle { base: f64, height: f64 },
}
fn main() {
let json = r#"{"type":"Circle","radius":5.0}"#;
let shape: Shape = serde_json::from_str(json).unwrap();
println!("{:?}", shape);
// Shape::Circle { radius: 5.0 }
let json = r#"{"type":"Rectangle","width":10.0,"height":20.0}"#;
let shape: Shape = serde_json::from_str(json).unwrap();
println!("{:?}", shape);
// Shape::Rectangle { width: 10.0, height: 20.0 }
// Deserialize directly - serde looks at "type" to determine variant
let json = r#"{"type":"Triangle","base":3.0,"height":4.0}"#;
let shape: Shape = serde_json::from_str(json).unwrap();
println!("{:?}", shape);
}Serde uses the tag field to determine which variant to deserialize.
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
enum Message {
Request { id: u32, method: String },
Response { id: u32, status: String },
}
fn main() {
// This works - both variants have "id" field
let json = r#"{"type":"Request","id":1,"method":"get"}"#;
let msg: Message = serde_json::from_str(json).unwrap();
println!("{:?}", msg);
// Tag field name must not conflict with variant fields
// If a variant had a "type" field, it would conflict:
// #[derive(Serialize, Deserialize)]
// #[serde(tag = "type")]
// enum Bad {
// Variant { type: String }, // Error: conflicts with tag!
// }
}The tag field name must not conflict with any variant's field names.
use serde::{Deserialize, Serialize};
// Internally tagged - tag inside the object
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
enum Internal {
Variant { data: String },
}
// Adjacently tagged - tag and data as siblings
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", content = "data")]
enum Adjacent {
Variant(String),
}
fn main() {
// Internal: {"type":"Variant","data":"hello"}
let internal = Internal::Variant { data: "hello".to_string() };
println!("Internal: {}", serde_json::to_string(&internal).unwrap());
// Adjacent: {"type":"Variant","data":"hello"}
let adjacent = Adjacent::Variant("hello".to_string());
println!("Adjacent: {}", serde_json::to_string(&adjacent).unwrap());
// Both look similar but handle variants differently
// Adjacent works with tuple variants; internal requires struct variants
}Adjacent tagging works with any variant type; internal requires struct variants.
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
enum Status {
Pending,
Active,
Completed,
}
fn main() {
let status = Status::Active;
let json = serde_json::to_string(&status).unwrap();
println!("{}", json);
// {"type":"Active"}
// Unit variants work as a special case
// They serialize as just the tag field
}Unit variants serialize to just the tag field with internally tagged representation.
use serde::{Deserialize, Serialize};
// This WON'T compile:
// #[derive(Debug, Serialize, Deserialize)]
// #[serde(tag = "type")]
// enum NewtypeEnum {
// Value(String), // Error: internally tagged enums cannot have newtype variants
// }
// Use adjacently tagged instead:
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", content = "value")]
enum NewtypeEnum {
Value(String),
}
fn main() {
let v = NewtypeEnum::Value("hello".to_string());
let json = serde_json::to_string(&v).unwrap();
println!("{}", json);
// {"type":"Value","value":"hello"}
}Internally tagged enums cannot have newtype or tuple variants directly.
use serde::{Deserialize, Serialize};
// Internally tagged requires struct variants (named fields)
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "kind")]
enum Animal {
Dog { name: String, breed: String },
Cat { name: String, lives: u8 },
Bird { name: String, can_fly: bool },
}
// This structure enables the tag to be inserted alongside fields
// Without named fields, there's nowhere to put the tag
fn main() {
let dog = Animal::Dog {
name: "Fido".to_string(),
breed: "Labrador".to_string(),
};
let json = serde_json::to_string(&dog).unwrap();
println!("{}", json);
// {"kind":"Dog","name":"Fido","breed":"Labrador"}
}Only struct variants (with named fields) work with internal tagging.
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "@type")] // Common in JSON-LD and some APIs
enum ApiObject {
Person { name: String, age: u32 },
Organization { name: String, employees: u32 },
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "event_type")]
enum LogEvent {
Login { user_id: u32 },
Logout { user_id: u32, duration_secs: u64 },
}
fn main() {
let person = ApiObject::Person {
name: "Alice".to_string(),
age: 30,
};
println!("{}", serde_json::to_string(&person).unwrap());
// {"@type":"Person","name":"Alice","age":30}
let event = LogEvent::Login { user_id: 42 };
println!("{}", serde_json::to_string(&event).unwrap());
// {"event_type":"Login","user_id":42}
}Choose tag field names that match your API's expectations.
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
enum Config {
Database { url: String },
Cache { ttl: u64 },
}
fn main() {
// Valid variant
let json = r#"{"type":"Database","url":"postgres://localhost"}"#;
let config: Result<Config, _> = serde_json::from_str(json);
println!("{:?}", config);
// Unknown variant
let json = r#"{"type":"Unknown","data":"value"}"#;
let config: Result<Config, _> = serde_json::from_str(json);
println!("{:?}", config);
// Err: unknown variant `Unknown`, expected `Database` or `Cache`
}Unknown variants produce clear error messages referencing expected variants.
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
enum Outer {
Inner(InnerEnum), // This requires adjacent or external tagging for InnerEnum
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "category")]
enum InnerEnum {
A { value: i32 },
B { value: String },
}
// Better approach: avoid nesting, flatten structure
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
enum FlatEvent {
OuterA { inner: i32 },
OuterB { inner: String },
}
fn main() {
// Nested enums work but may have confusing JSON structure
let outer = Outer::Inner(InnerEnum::A { value: 42 });
let json = serde_json::to_string(&outer).unwrap();
println!("{}", json);
// {"type":"Inner","category":"A","value":42}
}Nested internally tagged enums work but produce nested tag fields.
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum Event {
UserCreated { user_id: u32, email: String },
UserDeleted { user_id: u32 },
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
enum ApiResponse {
Success {
#[serde(rename = "data")]
result: String,
},
Error {
#[serde(rename = "error_message")]
message: String,
},
}
fn main() {
let event = Event::UserCreated {
user_id: 1,
email: "test@example.com".to_string(),
};
println!("{}", serde_json::to_string(&event).unwrap());
// {"type":"user_created","user_id":1,"email":"test@example.com"}
}Combine tag with rename_all and other attributes for API compatibility.
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
enum EventType {
Click { x: i32, y: i32 },
Scroll { delta: i32 },
}
#[derive(Debug, Serialize, Deserialize)]
struct Event {
timestamp: u64,
#[serde(flatten)]
event_type: EventType,
}
fn main() {
let event = Event {
timestamp: 1234567890,
event_type: EventType::Click { x: 100, y: 200 },
};
let json = serde_json::to_string(&event).unwrap();
println!("{}", json);
// {"timestamp":1234567890,"type":"Click","x":100,"y":200}
// The type field from EventType is flattened into the parent
}Flattening preserves the tag field, merging it into the parent structure.
use serde::{Deserialize, Serialize};
// Many APIs use a discriminator field pattern
// This is exactly what internally tagged enums match
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "kind")]
enum KubernetesResource {
Pod {
name: String,
namespace: String,
},
Service {
name: String,
port: u16,
},
Deployment {
name: String,
replicas: u32,
},
}
// GitHub API-style event
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "action")]
enum GitHubEvent {
Pushed { ref: String, commits: u32 },
Opened { number: u32, title: String },
Closed { number: u32, merged: bool },
}
fn main() {
// Kubernetes-style JSON
let json = r#"{"kind":"Pod","name":"my-pod","namespace":"default"}"#;
let resource: KubernetesResource = serde_json::from_str(json).unwrap();
println!("{:?}", resource);
}Many real-world APIs use this discriminator pattern naturally.
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
enum Message {
Request {
id: u32,
#[serde(skip_serializing_if = "Option::is_none")]
payload: Option<String>,
},
Response {
id: u32,
result: String,
},
}
fn main() {
let msg = Message::Request {
id: 1,
payload: None,
};
println!("{}", serde_json::to_string(&msg).unwrap());
// {"type":"Request","id":1}
let msg = Message::Request {
id: 2,
payload: Some("data".to_string()),
};
println!("{}", serde_json::to_string(&msg).unwrap());
// {"type":"Request","id":2,"payload":"data"}
}All serde attributes work normally with internally tagged enums.
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
enum Command {
Create { name: String },
Delete { id: u32 },
Update { id: u32, name: String },
}
fn parse_command(json: &str) -> Result<Command, serde_json::Error> {
serde_json::from_str(json)
}
fn main() {
// Missing tag
let result = parse_command(r#"{"name":"test"}"#);
match result {
Err(e) => println!("Error: {}", e),
// Error: missing field `type` at line 1 column 16
_ => {}
}
// Valid
let cmd = parse_command(r#"{"type":"Create","name":"test"}"#);
println!("{:?}", cmd);
// Extra fields are accepted by default
let cmd = parse_command(r#"{"type":"Create","name":"test","extra":true}"#);
println!("{:?}", cmd);
// Ok(Create { name: "test" })
}The tag field is required for deserialization; extra fields are ignored by default.
| Tagging Style | JSON Structure | Variant Types Supported |
|--------------|----------------|------------------------|
| External (default) | {"Variant": {...}} | All |
| Internal (tag) | {"type":"Variant",...} | Struct and unit only |
| Adjacent (tag+content) | {"type":"Variant","content":...} | All |
| Untagged | {...} (no discriminator) | All (requires unique fields) |
#[serde(tag = "type")] enables internally tagged enums by placing the variant discriminator as a field within the serialized object:
How it works: During serialization, serde adds the tag field (e.g., "type": "Click") alongside the variant's fields. During deserialization, serde reads the tag field first to determine which variant to deserialize into, then parses the remaining fields for that variant.
The constraint: Only struct variants (with named fields) and unit variants work. Newtype and tuple variants cannot be internally tagged because there's no structure to insert the tag intoāyou'd need adjacent tagging (#[serde(tag = "...", content = "...")]) for those.
When to use it:
When to avoid it:
Key insight: Internally tagged enums match how many real-world APIs structure their data. The discriminator field patternāwhere a type, kind, or @type field identifies the variantāis ubiquitous in JSON APIs, making serde(tag = "...") the natural choice for interoperating with such APIs. The representation produces cleaner, flatter JSON that matches what many APIs already use, eliminating the need for custom serialization logic to reshape the data.