Agent Skills: Serialization Patterns

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.

UncategorizedID: arustydev/ai/patterns-serialization-dev

Repository

aRustyDevLicense: AGPL-3.0
72

Install this agent skill to your local

pnpm dlx add-skill https://github.com/aRustyDev/agents/tree/HEAD/content/skills/patterns-serialization-dev

Skill Files

Browse the full folder contents for patterns-serialization-dev.

Download Skill

Loading file tree…

content/skills/patterns-serialization-dev/SKILL.md

Skill Metadata

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-*-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

Related Skills

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