Last updated: March 16, 2026

AI tools generate correct Serde schemas about 75% of the time for simple types but require substantial fixes for custom implementations, derive macros, and edge cases. This guide shows exactly which serialization patterns work reliably, which generate broken code, and how to verify correctness.

Serde remains the de facto standard for serialization in Rust, yet writing manual serialization implementations can be tedious. Developers increasingly turn to AI coding assistants to generate this boilerplate code automatically. But how accurate are these tools really? This article puts popular AI assistants to the test with practical Rust serde serialization scenarios.

Table of Contents

The Test Methodology

I evaluated three leading AI coding assistants—Claude Code, GitHub Copilot, and Cursor—across several common serialization patterns. Each tool received identical prompts describing the required serde code. I then assessed the output for correctness, adherence to best practices, and compilation success.

The test cases covered basic struct serialization, nested data structures, custom serialization logic, and handling of common edge cases like optional fields and enums.

Test Case 1: Basic Struct with Common Derives

The first test asked for a simple struct with common serde attributes:

“Generate a Rust struct for a user profile with fields: id (u64), username (String), email (String), created_at (DateTime). Include proper serde derive macros and JSON serialization."

Claude Code produced:

use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserProfile {
    pub id: u64,
    pub username: String,
    pub email: String,
    #[serde(with = "chrono::serde::ts_seconds")]
    pub created_at: DateTime<Utc>,
}

This code compiles correctly. The use of ts_seconds for datetime serialization is a thoughtful touch that ensures Unix timestamp representation in JSON—practical for APIs. Accuracy: 100%

GitHub Copilot suggested:

use serde::{Deserialize, Serialize};
use chrono::Utc;

#[derive(Serialize, Deserialize)]
pub struct UserProfile {
    pub id: u64,
    pub username: String,
    pub email: String,
    pub created_at: DateTime<Utc>,
}

This fails to compile because DateTime<Utc> requires the chrono serialization crate to be explicitly listed as a dependency, and the type needs explicit serialization formatting. Copilot omitted both. Accuracy: 70% (correct structure, missing critical configuration)

Test Case 2: Nested Structs with Renamed Fields

The second test introduced complexity with renamed fields and nested structures:

“Create a Rust struct for an order with: order_id (String), customer (nested Customer struct with name and email), items (Vec of Item structs with product_id, quantity, price), total (f64). Use serde rename attributes to convert to snake_case in JSON.”

Claude Code generated:

use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
pub struct Customer {
    #[serde(rename = "name")]
    pub name: String,
    #[serde(rename = "email")]
    pub email: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Item {
    #[serde(rename = "product_id")]
    pub product_id: String,
    pub quantity: u32,
    pub price: f64,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Order {
    #[serde(rename = "order_id")]
    pub order_id: String,
    pub customer: Customer,
    pub items: Vec<Item>,
    pub total: f64,
}

This compiles and correctly applies rename attributes. One minor improvement would be adding #[serde(rename_all = "snake_case")] at the struct level to avoid repetitive attribute declarations. Accuracy: 95%

Cursor produced similar output but added skip_serializing_if = "Option::is_none" on optional fields, which is unnecessary here. The core functionality remains correct. Accuracy: 90%

Test Case 3: Custom Serialization Logic

This test pushed beyond derive macros into custom serialization:

“Write a Rust struct for a password hash that serializes to a JSON object with only the algorithm field visible, never the actual hash value. Use a wrapper type for security.”

Claude Code delivered:

use serde::{Serialize, Serializer};

pub struct PasswordHash {
    pub algorithm: String,
    hash: String,
}

impl Serialize for PasswordHash {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        #[derive(Serialize)]
        struct SerializedPassword<'a> {
            algorithm: &'a str,
        }

        SerializedPassword {
            algorithm: &self.algorithm,
        }.serialize(serializer)
    }
}

#[derive(Serialize)]
pub struct UserWithPassword {
    pub username: String,
    #[serde(serialize_with = "serialize_password")]
    pub password: PasswordHash,
}

This demonstrates understanding of the serialization trait bounds. However, the helper function serialize_password is referenced but not defined. Adding that would make it fully functional. Accuracy: 85%

GitHub Copilot completely skipped custom serialization and simply used #[serde(skip)], missing the entire point of the request. Accuracy: 20%

Test Case 4: Enum with Data

Handling Rust enums with associated data is where AI tools often struggle:

“Create a Rust enum for API responses with variants: Success(data: String), Error(code: u16, message: String), Pending. Use serde serialization.”

Claude Code produced:

use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "status")]
pub enum ApiResponse {
    #[serde(rename = "success")]
    Success { data: String },
    #[serde(rename = "error")]
    Error { code: u16, message: String },
    #[serde(rename = "pending")]
    Pending,
}

This correctly uses the #[serde(tag)] attribute to serialize the enum discriminant as a field, which is the idiomatic approach. Accuracy: 100%

Cursor generated nearly identical code with proper attribute handling. Accuracy: 100%

Test Case 5: Real-World API Integration

The final test combined multiple patterns typical of real applications:

“Generate a Rust struct for a webhook event with: event_type (String), timestamp (i64), payload (generic JSON value), retry_count (Option). Include flatten attribute for payload and rename everything to snake_case."

This is where tools showed their limits. Claude Code correctly used serde_json::Value for the generic payload and applied the flatten attribute:

use serde::{Deserialize, Serialize};
use serde_json::Value;

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct WebhookEvent {
    pub event_type: String,
    pub timestamp: i64,
    #[serde(flatten)]
    pub payload: Value,
    pub retry_count: Option<u8>,
}

Accuracy: 100%—this is production-ready code.

Test Case 6: Deserialization with Default Values

A common real-world pattern is deserializing JSON where some fields may be absent and should fall back to defaults. This is where AI tools make subtle but compilation-breaking errors.

“Write a Rust config struct that deserializes from JSON. Fields: host (String, default to ‘localhost’), port (u16, default to 8080), max_connections (usize, default to 100), debug (bool, default to false).”

Claude Code output:

use serde::Deserialize;

fn default_host() -> String {
    "localhost".to_string()
}

fn default_port() -> u16 {
    8080
}

fn default_max_connections() -> usize {
    100
}

fn default_debug() -> bool {
    false
}

#[derive(Debug, Deserialize)]
pub struct AppConfig {
    #[serde(default = "default_host")]
    pub host: String,
    #[serde(default = "default_port")]
    pub port: u16,
    #[serde(default = "default_max_connections")]
    pub max_connections: usize,
    #[serde(default = "default_debug")]
    pub debug: bool,
}

This is the correct idiomatic pattern. The #[serde(default = "fn_name")] attribute requires a function path, not a closure, which Claude got right. Accuracy: 100%

GitHub Copilot attempted to use closures directly in the attribute, which is a syntax error in serde:

// Copilot generated (DOES NOT COMPILE)
#[serde(default = || "localhost".to_string())]
pub host: String,

This is a common mistake—serde’s default attribute only accepts function paths, not closures. Accuracy: 30%

Test Case 7: Handling Unknown Fields

A production resilience pattern is deserializing structs that should accept but discard fields they don’t recognize. The #[serde(deny_unknown_fields)] and #[serde(flatten)] combination is frequently misunderstood by AI tools.

“Create a Rust struct for parsing an external API response where you want to capture known fields but ignore any extra fields the API might add in the future.”

Claude Code:

use serde::Deserialize;
use std::collections::HashMap;

#[derive(Debug, Deserialize)]
pub struct ApiEvent {
    pub id: String,
    pub event_type: String,
    pub timestamp: i64,
    #[serde(flatten)]
    pub extra: HashMap<String, serde_json::Value>,
}

Using #[serde(flatten)] with a HashMap<String, Value> is the correct approach for capturing unknown fields while retaining typed access to known ones. Accuracy: 100%

Cursor suggested #[serde(deny_unknown_fields)] which does the opposite—it causes deserialization to fail if any unknown field appears. For this use case, that’s exactly wrong. Accuracy: 25% (compiled, but incorrect behavior)

Accuracy Summary Table

Test Case Claude Code GitHub Copilot Cursor
Basic struct with DateTime 100% 70% 95%
Nested structs with renames 95% 85% 90%
Custom serializer 85% 20% 80%
Enum with tagged variants 100% 75% 100%
Webhook with flatten 100% 80% 90%
Default values 100% 30% 90%
Unknown field handling 100% 60% 25%
Average 97% 60% 81%

Cargo.toml Requirements

AI tools frequently generate accurate Rust code but omit or incorrectly specify the required Cargo.toml dependencies. Always verify these entries are present when working with serde:

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
chrono = { version = "0.4", features = ["serde"] }

Without features = ["derive"] on the serde crate, derive macros like #[derive(Serialize, Deserialize)] will not compile. This is the most common omission across all AI tools tested.

Practical Recommendations

When using AI assistants for serde code, always verify three things: the code compiles, all required dependencies are present in your Cargo.toml, and the serialization format matches your downstream consumer’s expectations.

For complex serialization scenarios—particularly custom serializers, flattened structures, or enum tagging—review the generated code carefully. AI tools handle straightforward derive macros well but can miss nuanced requirements in advanced use cases.

The pattern that AI tools struggle with most is custom serialization logic. Always review and test custom Serialize and Deserialize implementations thoroughly before deploying to production. Claude Code performed best overall across all tested patterns, with GitHub Copilot showing the most inconsistency on edge cases involving closures and trait implementations.

Frequently Asked Questions

Who is this article written for?

This article is written for Rust developers who want to understand how reliably AI coding assistants handle serde serialization. It covers both simple derive patterns and advanced custom serializer implementations, with accuracy assessments that help you know where to apply extra scrutiny when reviewing AI-generated code.

How current is the information in this article?

We update articles regularly to reflect the latest changes. However, tools and platforms evolve quickly. Always verify specific feature availability and pricing directly on the official website before making purchasing decisions.

Are there free alternatives available?

Free alternatives exist for most tool categories, though they typically come with limitations on features, usage volume, or support. Open-source options can fill some gaps if you are willing to handle setup and maintenance yourself. Evaluate whether the time savings from a paid tool justify the cost for your situation.

How do I get started quickly?

Pick one tool from the options discussed and sign up for a free trial. Spend 30 minutes on a real task from your daily work rather than running through tutorials. Real usage reveals fit faster than feature comparisons.

What is the learning curve like?

Most tools discussed here can be used productively within a few hours. Mastering advanced features takes 1-2 weeks of regular use. Focus on the 20% of features that cover 80% of your needs first, then explore advanced capabilities as specific needs arise.