AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Data Validation Schema Patterns

skill-mickeyyaya-refactoring-skills-data-validation-schema-patterns · by mickeyyaya

Use when reviewing code for data validation correctness — covers validation boundary principle, TypeScript (Zod, Joi, io-ts), Python (Pydantic v2, marshmallow, dataclasses), Go (go-playground/validator), Java (Bean Validation / Hibernate Validator), schema evolution and compatibility, coercion vs strict parsing, custom validators, composable schemas, and validation anti-patterns with red flags an…

No reviews yet
0 installs
15 views
0.0% view→install

Install

$ agentstack add skill-mickeyyaya-refactoring-skills-data-validation-schema-patterns

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution Used

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-mickeyyaya-refactoring-skills-data-validation-schema-patterns)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
5mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Data Validation Schema Patterns? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Data Validation and Schema Patterns for Code Review

Overview

Unvalidated input is the root cause of injection attacks, data corruption, and unexpected crashes. Validation is not a one-time gate at the UI layer — every system boundary must enforce its own schema contract. Use this guide during code review to catch validation hazards before they ship.

When to use: Reviewing code that accepts external data (HTTP requests, queue messages, file uploads, CLI args, environment variables); designing API contracts; evaluating schema evolution for backward compatibility; auditing trust boundaries in internal services.

Quick Reference

| Pattern | Core Idea | Primary Red Flag | |---------|-----------|-----------------| | Validation Boundary | Validate at every trust boundary, not just at the UI | Passing raw unknown / any deep into business logic | | TypeScript / Zod | Runtime schema tied to compile-time type | z.any() escapes, skipping .parse() on external data | | TypeScript / Joi | Rich rule DSL with detailed error messages | .unknown(true) without explicit allow-list | | TypeScript / io-ts | Codec = decoder + encoder, composable | Ignoring left branch of Either decode result | | Python / Pydantic v2 | Model-first validation, high performance | model_config = {'arbitrary_types_allowed': True} masking issues | | Python / marshmallow | Schema-centric, explicit serialization control | load() result used without checking validation errors | | Python / dataclasses | Structural typing only, no runtime enforcement | Trusting @dataclass fields have the declared type at runtime | | Go / validator | Struct tag-based declarative rules | Missing binding:"required" tags on mandatory fields | | Java / Bean Validation | Annotation-driven, integrates with frameworks | @Valid missing on nested objects or method parameters | | Schema Evolution | Additive changes are safe; removals and renames break | Removing required fields without a deprecation cycle | | Strict vs Coercive | Coercion silently accepts wrong types; strict fails fast | z.coerce.number() accepting "abc"NaN | | Custom Validators | Encode business rules as first-class schema constraints | Business rule checks scattered outside schema definition |


Patterns in Detail

1. Validation Boundary Principle

Core Rule: Every system boundary — HTTP endpoint, queue consumer, file parser, CLI argument, environment variable, IPC call, or internal service API — is a trust boundary. Data that crosses a trust boundary must be validated before it is used.

Red Flags:

  • unknown or any typed parameters passed directly to repository or service functions
  • Validation only at the presentation layer; business logic assumes clean data
  • Internal microservice calls skipping validation because "we control both sides"
  • A single validation at ingestion time, then raw structs passed across multiple layers

The Trust Boundary Map:

External World
  → HTTP Request body        ← VALIDATE HERE
  → Query/path parameters    ← VALIDATE HERE
  → Message queue payload    ← VALIDATE HERE
  → File upload content      ← VALIDATE HERE
  → Env variables at startup ← VALIDATE HERE
  Internal Services
    → Inter-service HTTP      ← VALIDATE HERE (defense-in-depth)
    → DB query results        ← VALIDATE HERE (schema drift)

TypeScript — boundary enforcement with Zod:

// WRONG — accepts unknown and passes it through
async function createUser(body: unknown) {
  return userRepo.insert(body as User); // any shape can reach the DB
}

// CORRECT — validate at the HTTP boundary, then work with typed data
const CreateUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(1).max(100),
  role: z.enum(['admin', 'viewer']),
});

async function createUser(body: unknown): Promise {
  const input = CreateUserSchema.parse(body); // throws ZodError on failure
  return userRepo.insert(input);              // input is typed CreateUser
}

2. TypeScript Validation — Zod, Joi, io-ts

Zod

Red Flags:

  • z.any() or z.unknown() used as a shortcut instead of a proper schema
  • .safeParse() result used without checking .success
  • Schemas defined per-request instead of at module level (performance)
  • Missing .strict() — extra fields pass through silently

Pattern — schema as the single source of truth:

import { z } from 'zod';

// Define once; derive the TypeScript type from it
const OrderSchema = z.object({
  id: z.string().uuid(),
  items: z.array(
    z.object({
      sku: z.string().min(1),
      qty: z.number().int().positive(),
      priceUsd: z.number().nonnegative(),
    })
  ).min(1),
  shippingAddress: z.object({
    street: z.string(),
    city: z.string(),
    postalCode: z.string().regex(/^\d{5}$/),
  }),
}).strict(); // reject unknown keys

export type Order = z.infer; // no duplication

// Safe parse — return Result instead of throwing
function parseOrder(raw: unknown): { ok: true; order: Order } | { ok: false; errors: string[] } {
  const result = OrderSchema.safeParse(raw);
  if (!result.success) {
    return { ok: false, errors: result.error.errors.map(e => `${e.path.join('.')}: ${e.message}`) };
  }
  return { ok: true, order: result.data };
}
Joi

Red Flags:

  • .unknown(true) on root schema — any extra key is silently passed
  • abortEarly: true (default) — stops at first error; clients see one error at a time
  • No .label() on fields — error messages reference internal key names
import Joi from 'joi';

const schema = Joi.object({
  email: Joi.string().email().required().label('Email'),
  age: Joi.number().integer().min(18).max(120).required().label('Age'),
}).options({ allowUnknown: false, abortEarly: false }); // collect all errors

const { error, value } = schema.validate(input);
if (error) {
  const messages = error.details.map(d => d.message);
  throw new ValidationError(messages);
}
io-ts

Red Flags:

  • Ignoring the Left branch (validation failure) of Either
  • Using t.any or t.unknown in codec definitions
  • Decoding without the PathReporter for human-readable errors
import * as t from 'io-ts';
import { PathReporter } from 'io-ts/PathReporter';
import { isLeft } from 'fp-ts/Either';

const UserCodec = t.type({
  id: t.string,
  email: t.string,
  role: t.union([t.literal('admin'), t.literal('viewer')]),
});
export type User = t.TypeOf;

function decodeUser(raw: unknown): User {
  const result = UserCodec.decode(raw);
  if (isLeft(result)) {
    throw new ValidationError(PathReporter.report(result).join('; '));
  }
  return result.right;
}

3. Python Validation — Pydantic v2, marshmallow, dataclasses

Pydantic v2

Red Flags:

  • model_config = ConfigDict(arbitrary_types_allowed=True) used to suppress type errors
  • model.model_dump() then re-instantiating a model — unnecessary round-trip
  • Missing @field_validator for cross-field constraints
  • model_validate(data) called with trusted-but-unverified internal dicts
from pydantic import BaseModel, EmailStr, field_validator, model_validator
from typing import Literal

class OrderItem(BaseModel):
    sku: str
    qty: int
    price_usd: float

    @field_validator('qty')
    @classmethod
    def qty_must_be_positive(cls, v: int) -> int:
        if v  'Order':
        if not self.items:
            raise ValueError('order must have at least one item')
        return self

# Validate at boundary — raises ValidationError with detailed messages
order = Order.model_validate(raw_payload)
marshmallow

Red Flags:

  • schema.load() result stored without checking for ValidationError
  • many=True load without error handling per-item
  • dump_default masking missing required fields on output
from marshmallow import Schema, fields, validate, ValidationError, post_load

class UserSchema(Schema):
    email = fields.Email(required=True)
    name = fields.Str(required=True, validate=validate.Length(min=1, max=100))
    age = fields.Int(required=True, validate=validate.Range(min=18))

    @post_load
    def make_user(self, data, **kwargs):
        return User(**data)  # transform to domain object

schema = UserSchema()
try:
    user = schema.load(request_data)
except ValidationError as e:
    raise BadRequestError(e.messages)
dataclasses — limitation note

@dataclass provides structural convenience but NO runtime type enforcement. Use only for internal data transfer objects where the source is already validated.

from dataclasses import dataclass

# WRONG — treating dataclass as a validator
@dataclass
class Config:
    port: int
    host: str

cfg = Config(**raw_env_dict)  # port could be "abc" — no error raised

# CORRECT — validate with Pydantic first, then convert or use Pydantic directly
class ConfigModel(BaseModel):
    port: int
    host: str

cfg = ConfigModel.model_validate(raw_env_dict)

4. Go Validation — go-playground/validator and Custom

Red Flags:

  • Struct tags present but validate.Struct(s) never called
  • binding:"required" used in Gin/Echo but missing on nested struct fields
  • Validation errors cast to string directly — field path information lost
  • Custom validators registered globally but not tested in isolation
package validation

import (
    "fmt"
    "github.com/go-playground/validator/v10"
)

var validate = validator.New()

type CreateOrderRequest struct {
    UserID string      `json:"userId" validate:"required,uuid4"`
    Items  []OrderItem `json:"items"  validate:"required,min=1,dive"`
    Email  string      `json:"email"  validate:"required,email"`
}

type OrderItem struct {
    SKU string `json:"sku" validate:"required,min=1"`
    Qty int    `json:"qty" validate:"required,gt=0"`
}

func ValidateCreateOrder(req CreateOrderRequest) error {
    if err := validate.Struct(req); err != nil {
        var errs validator.ValidationErrors
        if ok := errors.As(err, &errs); ok {
            msgs := make([]string, len(errs))
            for i, e := range errs {
                msgs[i] = fmt.Sprintf("%s: failed %s", e.Field(), e.Tag())
            }
            return fmt.Errorf("validation failed: %s", strings.Join(msgs, "; "))
        }
        return err
    }
    return nil
}

// Custom validator registered at startup
func init() {
    _ = validate.RegisterValidation("sku_format", func(fl validator.FieldLevel) bool {
        return skuRegexp.MatchString(fl.Field().String())
    })
}

5. Java Validation — Bean Validation and Hibernate Validator

Red Flags:

  • @Valid missing on nested object parameters — nested fields not validated
  • @NotNull used on primitive types — redundant; @NotBlank needed for strings
  • Controller method parameter missing @Valid — Spring skips validation silently
  • Custom ConstraintValidator not registered in ValidationConfig
  • javax.validation (old) mixed with jakarta.validation (new) in the same project
import jakarta.validation.Valid;
import jakarta.validation.constraints.*;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

// Domain model with Bean Validation annotations
public class CreateOrderRequest {

    @NotBlank(message = "userId is required")
    private String userId;

    @Valid                           // propagates validation into nested objects
    @NotEmpty(message = "items required")
    @Size(min = 1, message = "at least one item required")
    private List items;

    @Email(message = "invalid email format")
    @NotBlank
    private String email;

    // getters / setters
}

public class OrderItem {
    @NotBlank
    private String sku;

    @Positive(message = "qty must be positive")
    private int qty;
}

// Spring controller — @Valid triggers Bean Validation
@RestController
public class OrderController {

    @PostMapping("/orders")
    public ResponseEntity createOrder(@Valid @RequestBody CreateOrderRequest req,
                                                     BindingResult result) {
        if (result.hasErrors()) {
            List errors = result.getFieldErrors().stream()
                .map(e -> e.getField() + ": " + e.getDefaultMessage())
                .collect(Collectors.toList());
            throw new ValidationException(errors);
        }
        return ResponseEntity.ok(orderService.create(req));
    }
}

// Custom constraint — Hibernate Validator extension
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = SkuFormatValidator.class)
public @interface ValidSku {
    String message() default "invalid SKU format";
    Class[] groups() default {};
    Class[] payload() default {};
}

public class SkuFormatValidator implements ConstraintValidator {
    private static final Pattern SKU_PATTERN = Pattern.compile("^[A-Z]{3}-\\d{4}$");

    @Override
    public boolean isValid(String value, ConstraintValidatorContext ctx) {
        return value != null && SKU_PATTERN.matcher(value).matches();
    }
}

6. Schema Evolution and Compatibility

Schema evolution governs how validation schemas change over time without breaking existing clients.

Additive-safe changes (backward compatible — old clients still pass validation):

  • Adding optional fields with defaults
  • Widening accepted value ranges
  • Adding new enum values (if consumers use unknown-value fallbacks)

Breaking changes (require deprecation cycle or API versioning):

  • Removing a field
  • Renaming a field
  • Narrowing accepted value range (e.g., max=1000max=500)
  • Adding a new required field
  • Changing a field's type

Red Flags:

  • Required field added to an existing schema without bumping API version
  • Enum values removed without checking all consumers
  • Field renamed in schema but consumers still send the old key
  • No schema version field — impossible to migrate gracefully

Pattern — additive evolution with default:

// v1 schema
const UserV1 = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
});

// v2 schema — additive: role is optional with a default
const UserV2 = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  role: z.enum(['admin', 'viewer']).default('viewer'), // safe — old clients omit field
});

// v2 schema — BREAKING: makes name required
const UserV2Breaking = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  name: z.string(), // old clients don't send this — BREAKING CHANGE
});

Python / Pydantic — forward-compatible model:

from pydantic import BaseModel
from typing import Optional

class UserV2(BaseModel):
    model_config = {'extra': 'ignore'}  # tolerate unknown fields from newer producers

    id: str
    email: str
    role: str = 'viewer'          # safe default for old producers
    display_name: Optional[str] = None  # new optional field

7. Coercion vs Strict Parsing

Coercion automatically converts values to the target type ("42"42). It is convenient but can mask data quality issues and silently accept malformed inputs.

Strict parsing rejects any value that is not already the correct type. Prefer strict mode at system boundaries.

Red Flags:

  • z.coerce.number() accepting ""0 or "abc"NaN
  • Pydantic coercing "false"False on a boolean field when the source is an HTTP header that should be 0/1
  • Coercion hiding producer bugs: producer sends a string, consumer silently converts — the bug is never fixed
// WRONG — coercion masks producer bugs
const Schema = z.object({
  count: z.coerce.number(), // "abc" → NaN, "" → 0; both silently accepted
});

// CORRECT — strict; reject non-numbers at the boundary
const StrictSchema = z.object({
  count: z.number().int().nonnegative(),
});

// ACCEPTABLE — coercion only for known string-to-number scenarios (e.g., quer

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [mickeyyaya](https://github.com/mickeyyaya)
- **Source:** [mickeyyaya/refactoring-skills](https://github.com/mickeyyaya/refactoring-skills)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.