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.
When & Why to Use This Skill
This Claude skill provides a comprehensive cross-language reference for serialization and validation patterns. It facilitates the seamless translation of data handling logic—including JSON/YAML/TOML serialization, struct tags, and validation rules—across major programming languages like TypeScript, Python, Rust, Go, and Java. By offering direct mappings for popular libraries such as Pydantic, Serde, and Jackson, it ensures consistent schema enforcement and data integrity during code conversion and multi-language system development.
Use Cases
- Migrating Data Models: Seamlessly convert Python Pydantic models to Rust Serde structs while preserving field aliases, default values, and complex validation constraints.
- Cross-Language API Development: Ensure consistent JSON serialization and validation logic when implementing the same API contract across different stacks like Go and TypeScript.
- Schema-Based Code Translation: Map validation decorators from Java (Bean Validation) to Python (Pydantic) or TypeScript (class-validator) during architectural refactoring or porting.
- Polyglot Microservices Design: Standardize data serialization patterns and custom validation rules (e.g., regex, range checks) across a diverse service mesh to maintain data consistency.
| name | patterns-serialization-dev |
|---|---|
| description | 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. |
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-*-devskills) - Protocol buffers/gRPC (see dedicated skills)
- Database ORM mapping (see database-specific skills)
Serialization Mechanism Comparison
| Language | Primary Approach | Configuration | Validation |
|---|---|---|---|
| TypeScript | Class decorators | class-transformer |
class-validator, zod |
| Python | Dataclasses/Pydantic | Type hints + decorators | Built into Pydantic |
| Rust | Derive macros | #[serde(...)] attributes |
validator crate |
| Go | Struct tags | `json:"name"` |
validator package |
| Java/Kotlin | Annotations | @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
| Language | Syntax | Example |
|---|---|---|
| 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
| Language | Syntax | Result |
|---|---|---|
| TypeScript | Manual or transformer | N/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
| Language | Syntax | Effect |
|---|---|---|
| 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
| Validation | TypeScript | Python | Rust | Go |
|---|---|---|---|---|
| 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=..." |
@IsEmail() |
EmailStr |
#[validate(email)] |
validate:"email" |
|
| URL | @IsUrl() |
HttpUrl |
#[validate(url)] |
validate:"url" |
Numeric Validation
| Validation | TypeScript | Python | Rust | Go |
|---|---|---|---|---|
| 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
| Language | Syntax | Example |
|---|---|---|
| TypeScript | Property initializer | status: string = 'pending' |
| Python | Field(default=...) |
status: str = Field(default='pending') |
| Rust | #[serde(default)] |
#[serde(default = "default_status")] |
| Go | Not in struct tags | Manual in constructor |
| Java | Field 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
| Category | TypeScript | Python | Rust | Go |
|---|---|---|---|---|
| JSON | Built-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
| TypeScript | Python | Rust | Go | Java |
|---|---|---|---|---|
class-validator |
Pydantic (built-in) | validator |
go-playground/validator |
Bean Validation |
zod |
pydantic |
garde |
ozzo-validation |
Hibernate Validator |
yup |
cerberus |
- | - | - |
joi |
marshmallow |
- | - | - |
Schema Generation
| TypeScript | Python | Rust | Go |
|---|---|---|---|
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
| Language | Type | JSON Format | Custom Format |
|---|---|---|---|
| TypeScript | Date |
ISO string | @Transform() |
| Python | datetime |
ISO string | @field_serializer() |
| Rust | DateTime<Utc> |
ISO string | #[serde(with = "...")] |
| Go | time.Time |
RFC3339 | Custom 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
- Validate at boundaries - Only validate external input, trust internal types
- Use schema-first when possible for API contracts
- Prefer declarative over imperative validation
- Keep serialization pure - No side effects in serializers
- Document format expectations in struct/class comments
- Test edge cases - Empty strings, nulls, malformed dates
- Version your schemas for backward compatibility
Related Skills
meta-convert-dev- Code conversion patternspatterns-metaprogramming-dev- Decorators/macros used for serializationlang-*-devskills - Language-specific serialization details