Agents patterns-serialization-dev

Cross-cutting patterns for serialization and validation across languages. Use when translating JSON handling between languages, converting struct tags to derive macros, mapping validation libraries, or designing schema-based serialization for language conversions.

install
source · Clone the upstream repo
git clone https://github.com/aRustyDev/agents
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/aRustyDev/agents "$T" && mkdir -p ~/.claude/skills && cp -r "$T/content/skills/patterns-serialization-dev" ~/.claude/skills/arustydev-agents-patterns-serialization-dev && rm -rf "$T"
manifest: content/skills/patterns-serialization-dev/SKILL.md
source content

Serialization Patterns

Cross-language reference for serialization, deserialization, and validation patterns. This skill helps translate data handling patterns between languages during code conversion.

Overview

This skill covers:

  • JSON/YAML/TOML serialization comparison
  • Struct tags, derive macros, dataclasses
  • Validation library mappings
  • Schema generation and enforcement
  • Custom serializer/deserializer patterns

This skill does NOT cover:

  • Building applications with serialization (see
    lang-*-dev
    skills)
  • Protocol buffers/gRPC (see dedicated skills)
  • Database ORM mapping (see database-specific skills)

Serialization Mechanism Comparison

LanguagePrimary ApproachConfigurationValidation
TypeScriptClass decorators
class-transformer
class-validator
,
zod
PythonDataclasses/PydanticType hints + decoratorsBuilt into Pydantic
RustDerive macros
#[serde(...)]
attributes
validator
crate
GoStruct tags
`json:"name"`
validator
package
Java/KotlinAnnotations
@JsonProperty
Bean Validation
C#Attributes
[JsonPropertyName]
Data Annotations

JSON Serialization by Language

TypeScript

// class-transformer + class-validator
import { Type, Expose, Transform } from 'class-transformer';
import { IsEmail, Length, IsOptional } from 'class-validator';

class User {
  @Expose({ name: 'user_id' })
  id: string;

  @Length(1, 100)
  name: string;

  @IsEmail()
  @IsOptional()
  email?: string;

  @Type(() => Date)
  @Transform(({ value }) => new Date(value), { toClassOnly: true })
  createdAt: Date;
}

// Usage
const user = plainToInstance(User, jsonData);
const errors = await validate(user);

Python (Pydantic)

from pydantic import BaseModel, Field, EmailStr, field_validator
from datetime import datetime
from typing import Optional

class User(BaseModel):
    id: str = Field(alias='user_id')
    name: str = Field(min_length=1, max_length=100)
    email: Optional[EmailStr] = None
    created_at: datetime

    @field_validator('name')
    @classmethod
    def name_must_not_be_empty(cls, v: str) -> str:
        if not v.strip():
            raise ValueError('name cannot be empty')
        return v

    class Config:
        populate_by_name = True  # Allow both 'id' and 'user_id'

# Usage
user = User.model_validate(json_data)
json_str = user.model_dump_json()

Python (dataclasses + dacite)

from dataclasses import dataclass, field
from typing import Optional
from datetime import datetime
from dacite import from_dict, Config

@dataclass
class User:
    id: str
    name: str
    email: Optional[str] = None
    created_at: datetime = field(default_factory=datetime.now)

# Usage
user = from_dict(User, json_data, config=Config(
    cast=[datetime],
    strict=True
))

Rust (Serde)

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

#[derive(Debug, Serialize, Deserialize, Validate)]
#[serde(rename_all = "snake_case")]
struct User {
    #[serde(rename = "user_id")]
    id: String,

    #[validate(length(min = 1, max = 100))]
    name: String,

    #[serde(skip_serializing_if = "Option::is_none")]
    #[validate(email)]
    email: Option<String>,

    #[serde(with = "chrono::serde::ts_seconds")]
    created_at: DateTime<Utc>,
}

// Usage
let user: User = serde_json::from_str(&json_str)?;
user.validate()?;
let json = serde_json::to_string(&user)?;

Go

import (
    "encoding/json"
    "time"

    "github.com/go-playground/validator/v10"
)

type User struct {
    ID        string    `json:"user_id"`
    Name      string    `json:"name" validate:"required,min=1,max=100"`
    Email     *string   `json:"email,omitempty" validate:"omitempty,email"`
    CreatedAt time.Time `json:"created_at"`
}

// Usage
var user User
err := json.Unmarshal(jsonData, &user)

validate := validator.New()
err = validate.Struct(user)

Java (Jackson)

import com.fasterxml.jackson.annotation.*;
import jakarta.validation.constraints.*;
import java.time.Instant;

public class User {
    @JsonProperty("user_id")
    private String id;

    @NotBlank
    @Size(min = 1, max = 100)
    private String name;

    @Email
    private String email;

    @JsonFormat(shape = JsonFormat.Shape.STRING)
    private Instant createdAt;

    // getters, setters...
}

// Usage
ObjectMapper mapper = new ObjectMapper();
User user = mapper.readValue(jsonStr, User.class);

Field Mapping Translation

Rename Fields

LanguageSyntaxExample
TypeScript
@Expose({ name: 'x' })
@Expose({ name: 'user_id' })
Python
Field(alias='x')
id: str = Field(alias='user_id')
Rust
#[serde(rename = "x")]
#[serde(rename = "user_id")]
Go
`json:"x"`
`json:"user_id"`
Java
@JsonProperty("x")
@JsonProperty("user_id")

Case Conversion

LanguageSyntaxResult
TypeScriptManual or transformerN/A
Python
model_config = ConfigDict(alias_generator=to_camel)
userId
Rust
#[serde(rename_all = "camelCase")]
userId
Go
`json:"userId"`
(manual)
userId
Java
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
user_id

Skip Null/Empty

LanguageSyntaxEffect
TypeScript
@Exclude()
with condition
Excludes field
Python
exclude_none=True
in
model_dump()
Skips None
Rust
#[serde(skip_serializing_if = "Option::is_none")]
Skips None
Go
json:",omitempty"
Skips zero values
Java
@JsonInclude(Include.NON_NULL)
Skips null

Validation Pattern Translation

String Validation

ValidationTypeScriptPythonRustGo
Required
@IsNotEmpty()
str
(not Optional)
Not Option
validate:"required"
Min length
@Length(min, max)
Field(min_length=n)
#[validate(length(min = n))]
validate:"min=n"
Max length
@Length(min, max)
Field(max_length=n)
#[validate(length(max = n))]
validate:"max=n"
Regex
@Matches(regex)
Field(pattern=r'...')
#[validate(regex = "...")]
validate:"regexp=..."
Email
@IsEmail()
EmailStr
#[validate(email)]
validate:"email"
URL
@IsUrl()
HttpUrl
#[validate(url)]
validate:"url"

Numeric Validation

ValidationTypeScriptPythonRustGo
Min value
@Min(n)
Field(ge=n)
#[validate(range(min = n))]
validate:"gte=n"
Max value
@Max(n)
Field(le=n)
#[validate(range(max = n))]
validate:"lte=n"
Positive
@IsPositive()
Field(gt=0)
#[validate(range(min = 1))]
validate:"gt=0"
Negative
@IsNegative()
Field(lt=0)
#[validate(range(max = -1))]
validate:"lt=0"

Custom Validation

TypeScript:

@ValidatorConstraint()
class IsAdultConstraint implements ValidatorConstraintInterface {
  validate(age: number) {
    return age >= 18;
  }
  defaultMessage() {
    return 'Must be 18 or older';
  }
}

@Validate(IsAdultConstraint)
age: number;

Python (Pydantic):

@field_validator('age')
@classmethod
def must_be_adult(cls, v: int) -> int:
    if v < 18:
        raise ValueError('Must be 18 or older')
    return v

Rust:

fn validate_adult(age: &i32) -> Result<(), validator::ValidationError> {
    if *age < 18 {
        return Err(validator::ValidationError::new("must_be_adult"));
    }
    Ok(())
}

#[validate(custom(function = "validate_adult"))]
age: i32,

Go:

func isAdult(fl validator.FieldLevel) bool {
    return fl.Field().Int() >= 18
}

validate.RegisterValidation("adult", isAdult)

type Person struct {
    Age int `validate:"adult"`
}

Nested Object Handling

TypeScript

class Address {
  @IsString()
  street: string;
}

class User {
  @ValidateNested()
  @Type(() => Address)
  address: Address;

  @ValidateNested({ each: true })
  @Type(() => Address)
  addresses: Address[];
}

Python

class Address(BaseModel):
    street: str

class User(BaseModel):
    address: Address
    addresses: list[Address]

Rust

#[derive(Serialize, Deserialize, Validate)]
struct Address {
    street: String,
}

#[derive(Serialize, Deserialize, Validate)]
struct User {
    #[validate(nested)]
    address: Address,

    #[validate(nested)]
    addresses: Vec<Address>,
}

Go

type Address struct {
    Street string `json:"street" validate:"required"`
}

type User struct {
    Address   Address   `json:"address" validate:"required"`
    Addresses []Address `json:"addresses" validate:"dive"`
}

Default Values

LanguageSyntaxExample
TypeScriptProperty initializer
status: string = 'pending'
Python
Field(default=...)
status: str = Field(default='pending')
Rust
#[serde(default)]
#[serde(default = "default_status")]
GoNot in struct tagsManual in constructor
JavaField initializer
private String status = "pending";

Rust default function:

fn default_status() -> String {
    "pending".to_string()
}

#[derive(Deserialize)]
struct Order {
    #[serde(default = "default_status")]
    status: String,
}

Library Mapping

Serialization Libraries

CategoryTypeScriptPythonRustGo
JSONBuilt-in
json
serde_json
encoding/json
YAML
js-yaml
pyyaml
serde_yaml
gopkg.in/yaml.v3
TOML
@iarna/toml
tomli
/
tomllib
toml
github.com/BurntSushi/toml
MessagePack
@msgpack/msgpack
msgpack
rmp-serde
github.com/vmihailenco/msgpack

Validation Libraries

TypeScriptPythonRustGoJava
class-validator
Pydantic (built-in)
validator
go-playground/validator
Bean Validation
zod
pydantic
garde
ozzo-validation
Hibernate Validator
yup
cerberus
---
joi
marshmallow
---

Schema Generation

TypeScriptPythonRustGo
typescript-json-schema
pydantic
(built-in)
schemars
github.com/invopop/jsonschema
zod-to-json-schema
datamodel-code-generator
--

Common Patterns

Optional vs Required

TypeScript:  name?: string          → Optional
Python:      name: Optional[str]    → Optional
Rust:        name: Option<String>   → Optional
Go:          Name *string           → Optional (pointer)

Date/Time Handling

LanguageTypeJSON FormatCustom Format
TypeScript
Date
ISO string
@Transform()
Python
datetime
ISO string
@field_serializer()
Rust
DateTime<Utc>
ISO string
#[serde(with = "...")]
Go
time.Time
RFC3339Custom
MarshalJSON

Enums

TypeScript:

enum Status { Pending = 'pending', Active = 'active' }

class Order {
  @IsEnum(Status)
  status: Status;
}

Python:

from enum import Enum

class Status(str, Enum):
    pending = 'pending'
    active = 'active'

class Order(BaseModel):
    status: Status

Rust:

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
enum Status {
    Pending,
    Active,
}

Go:

type Status string

const (
    StatusPending Status = "pending"
    StatusActive  Status = "active"
)

Anti-Patterns

1. Mixing Validation and Business Logic

# ❌ Business logic in validator
@field_validator('age')
def check_age(cls, v):
    if v < 18:
        send_notification("underage user attempted")  # Side effect!
        raise ValueError('underage')
    return v

# ✓ Separate concerns
@field_validator('age')
def check_age(cls, v):
    if v < 18:
        raise ValueError('underage')
    return v

# Business logic elsewhere
if not user.is_adult():
    send_notification(...)

2. Over-validation

# ❌ Validating internal types
class InternalData(BaseModel):
    # This is only used internally, no need for extensive validation
    temp_id: str = Field(regex=r'^[a-f0-9]{32}$')

# ✓ Validate at boundaries only
class UserInput(BaseModel):  # External input
    email: EmailStr  # Validate here

class InternalData:  # Internal use
    temp_id: str  # Trust internal code

3. Ignoring Serialization Errors

// ❌ Ignoring errors
json.Unmarshal(data, &user)  // Error ignored!

// ✓ Handle errors
if err := json.Unmarshal(data, &user); err != nil {
    return fmt.Errorf("invalid user data: %w", err)
}

Best Practices

  1. Validate at boundaries - Only validate external input, trust internal types
  2. Use schema-first when possible for API contracts
  3. Prefer declarative over imperative validation
  4. Keep serialization pure - No side effects in serializers
  5. Document format expectations in struct/class comments
  6. Test edge cases - Empty strings, nulls, malformed dates
  7. Version your schemas for backward compatibility

Related Skills

  • meta-convert-dev
    - Code conversion patterns
  • patterns-metaprogramming-dev
    - Decorators/macros used for serialization
  • lang-*-dev
    skills - Language-specific serialization details