patterns-serialization-dev

aRustyDev's avatarfrom aRustyDev

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.

⭐1stars🔀1forks📁View on GitHub🕐Updated Jan 4, 2026

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.
namepatterns-serialization-dev
descriptionCross-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-*-dev skills)
  • 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=..."
Email @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

  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

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