npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@a-company/atelier-schema

v0.26.0

Published

Zod validation schemas with AI-readable error messages

Readme


title: "@atelier/schema" scope: Zod validation schemas, YAML parse/serialize, AI-readable error formatting packages: ["@atelier/schema"] related: ["docs/format-spec.md", "packages/types/README.md", "packages/core/README.md"]

@atelier/schema

Zod validation schemas for the Atelier animation document format. Every type in @atelier/types has a corresponding runtime schema here, plus validation functions that return flat, AI-readable errors and YAML parse/serialize utilities.

Package Info

| Field | Value | |-------|-------| | Name | @atelier/schema | | Version | 0.1.0 | | Build | tsup (ESM + CJS + DTS) | | Source | packages/schema/src/ | | Test | vitest run |

Dependencies

| Package | Version | |---------|---------| | @atelier/types | workspace:* | | zod | ^3.24.0 | | yaml | ^2.7.0 |

Architecture

@atelier/types (TypeScript interfaces)
        |
        v
@atelier/schema (Zod runtime schemas)
   |         |
   v         v
validate  parse/serialize
   |         |
   +----+----+
        |
        v
  ValidationResult<T>
  { success, data | errors }

All validation paths converge on a single ValidationResult<T> type. Whether you validate a JS object or parse YAML, you get the same result shape with flat {path, message} errors.

Exports

Zod Schemas

Every schema mirrors a type from @atelier/types one-to-one.

Units (src/units.ts)

| Schema | Validates | |--------|-----------| | PixelSchema | Any number (pixel value) | | PercentageSchema | String matching /^-?\d+(\.\d+)?%$/ (e.g. "50%") | | UnitValueSchema | Union of PixelSchema \| PercentageSchema |

Coordinates (src/coordinates.ts)

| Schema | Validates | |--------|-----------| | FrameSchema | { x: UnitValue, y: UnitValue } | | BoundsSchema | { width: UnitValue, height: UnitValue } | | AnchorPointSchema | { x: 0..1, y: 0..1 } |

Color (src/color.ts)

| Schema | Validates | |--------|-----------| | RGBAColorSchema | { r: 0-255, g: 0-255, b: 0-255, a: 0-1 } | | HSLAColorSchema | { h: 0-360, s: 0-100, l: 0-100, a: 0-1 } | | HexColorSchema | Hex string: #RGB, #RGBA, #RRGGBB, or #RRGGBBAA | | ColorSchema | Union of RGBA \| HSLA \| Hex |

Shape & Fill (src/shape.ts)

| Schema | Validates | |--------|-----------| | PathPointSchema | { x, y } with optional in/out control points | | RectShapeSchema | { type: "rect" } with optional cornerRadius (number or 4-tuple) | | EllipseShapeSchema | { type: "ellipse" } | | PathShapeSchema | { type: "path", points: [...] } (min 2 points), optional closed | | ShapeSchema | Discriminated union on type: rect, ellipse, path | | GradientStopSchema | { offset: 0-1, color: Color } | | SolidFillSchema | { type: "solid", color: Color } | | LinearGradientFillSchema | { type: "linear-gradient", angle, stops } (min 2 stops) | | RadialGradientFillSchema | { type: "radial-gradient", center, radius, stops } (min 2 stops) | | FillSchema | Discriminated union on type: solid, linear-gradient, radial-gradient | | StrokeSchema | { color, width } with optional dash, lineCap, lineJoin | | TextStyleSchema | { fontFamily, fontSize, color } with optional weight, style, align, etc. |

Easing (src/easing.ts)

| Schema | Validates | |--------|-----------| | LinearEasingSchema | { type: "linear" } | | CubicBezierEasingSchema | { type: "cubic-bezier", x1, y1, x2, y2 } (x1/x2 clamped 0-1) | | SpringEasingSchema | { type: "spring" } with optional mass, stiffness, damping, velocity | | StepEasingSchema | { type: "step", steps } with optional position (start/end) | | EasingPresetSchema | Enum: "ease-in", "ease-out", "ease-in-out" | | EasingSchema | Union of all easing types + presets |

Layer (src/layer.ts)

| Schema | Validates | |--------|-----------| | ShapeVisualSchema | { type: "shape", shape } with optional fill/stroke | | TextVisualSchema | { type: "text", content, style } | | ImageVisualSchema | { type: "image", assetId } | | GroupVisualSchema | { type: "group" } | | RefVisualSchema | { type: "ref", src } | | VisualSchema | Discriminated union on type: shape, text, image, group, ref | | LayerSchema | Full layer: { id, visual, frame, bounds } + optional fields |

Delta (src/delta.ts)

| Schema | Validates | |--------|-----------| | AnimatablePropertySchema | Enum of animatable dot-paths (e.g. "opacity", "frame.x", "scale.y") | | FrameRangeSchema | [start, end] tuple where end >= start (both non-negative integers) | | DeltaSchema | { layer, property, range, from, to } with optional easing, id, description, tags |

State (src/state.ts)

| Schema | Validates | |--------|-----------| | StateSchema | { duration, deltas: Delta[] } with optional description, tags |

Preset (src/preset.ts)

| Schema | Validates | |--------|-----------| | PresetDeltaSchema | { property, from, to } with optional offset and easing | | PresetSchema | { deltas: PresetDelta[] } (min 1 delta) with optional description, tags |

Variable (src/variable.ts)

| Schema | Validates | |--------|-----------| | VariableTypeSchema | Enum: "string", "number", "color", "asset", "boolean" | | VariableSchema | { type: VariableType } with optional default, description |

Asset (src/asset.ts)

| Schema | Validates | |--------|-----------| | AssetTypeSchema | Enum: "image", "svg", "font", "animation" | | AssetSchema | { type: AssetType, src } with optional description |

Document (src/document.ts)

| Schema | Validates | |--------|-----------| | CanvasSchema | { width, height, fps } (positive integers) with optional background | | AtelierDocumentSchema | Full document: { version, name, canvas, layers, states } with optional description, tags, variables, assets, presets |

Validation Functions (src/validate.ts)

type ValidationResult<T> =
  | { success: true; data: T }
  | { success: false; errors: ValidationError[] };

interface ValidationError {
  path: string;
  message: string;
}

function validateDocument(input: unknown): ValidationResult<AtelierDocument>;
function validateLayer(input: unknown): ValidationResult<Layer>;
function validateDelta(input: unknown): ValidationResult<Delta>;

All three functions follow the same pattern: call safeParse on the corresponding Zod schema, then flatten any Zod issues into {path, message} pairs via an internal formatErrors() function. The path is built from issue.path.join("."), falling back to "(root)" when the issue has no path segments.

YAML Parsing (src/parse.ts)

function parseAtelier(yamlString: string): ValidationResult<AtelierDocument>;
function serializeAtelier(doc: AtelierDocument): string;

parseAtelier performs YAML parse followed by validateDocument() in one step. If the YAML itself is malformed, it returns a single error with path: "(yaml)" and the parse error message. Otherwise, it delegates to validateDocument and returns whatever schema errors apply.

serializeAtelier converts a validated AtelierDocument to a YAML string using yaml.stringify with indent: 2.

Roundtrip fidelity is tested: parse, validate, serialize, re-parse produces matching data.

Usage Examples

1. Validating a Document

import { validateDocument } from "@atelier/schema";

const result = validateDocument({
  version: "1.0",
  name: "my-animation",
  canvas: { width: 1080, height: 1080, fps: 30 },
  layers: [],
  states: {},
});

if (result.success) {
  console.log(result.data.name); // "my-animation"
} else {
  console.error(result.errors);
}

2. Parsing YAML

import { parseAtelier } from "@atelier/schema";

const yaml = `
version: "1.0"
name: fade-in
canvas:
  width: 1920
  height: 1080
  fps: 60
layers:
  - id: bg
    visual:
      type: shape
      shape:
        type: rect
      fill:
        type: solid
        color: "#000000"
    frame: { x: 0, y: 0 }
    bounds: { width: 1920, height: 1080 }
states:
  idle:
    duration: 30
    deltas: []
`;

const result = parseAtelier(yaml);
if (result.success) {
  console.log(result.data.layers[0].id); // "bg"
}

3. Serializing to YAML

import { validateDocument, serializeAtelier } from "@atelier/schema";

const result = validateDocument({
  version: "1.0",
  name: "bounce",
  canvas: { width: 1080, height: 1080, fps: 30 },
  layers: [],
  states: {},
});

if (result.success) {
  const yaml = serializeAtelier(result.data);
  console.log(yaml);
  // version: "1.0"
  // name: bounce
  // canvas:
  //   width: 1080
  //   height: 1080
  //   fps: 30
  // layers: []
  // states: {}
}

4. Handling Validation Errors (Flat Format)

Errors are flat {path, message} objects -- no nested Zod error trees. This makes them easy to log, display, or feed to an AI model.

import { validateDocument } from "@atelier/schema";

const result = validateDocument({ name: "test" });
// result:
// {
//   success: false,
//   errors: [
//     { path: "version", message: "Required" },
//     { path: "canvas", message: "Required" },
//     { path: "layers", message: "Required" },
//     { path: "states", message: "Required" }
//   ]
// }

if (!result.success) {
  for (const err of result.errors) {
    console.error(`${err.path}: ${err.message}`);
  }
  // version: Required
  // canvas: Required
  // layers: Required
  // states: Required
}

YAML parse errors follow the same shape:

import { parseAtelier } from "@atelier/schema";

const result = parseAtelier("{{{{ not yaml");
// { success: false, errors: [{ path: "(yaml)", message: "YAML parse error: ..." }] }

5. Using Individual Schemas for Partial Validation

You can import any schema directly for ad-hoc validation of fragments:

import { LayerSchema, DeltaSchema, ColorSchema } from "@atelier/schema";

// Validate a single layer
const layerResult = LayerSchema.safeParse({
  id: "circle",
  visual: { type: "shape", shape: { type: "ellipse" } },
  frame: { x: "50%", y: "50%" },
  bounds: { width: 200, height: 200 },
});

// Validate a color value
const colorResult = ColorSchema.safeParse("#FF5500");

// Validate a delta
const deltaResult = DeltaSchema.safeParse({
  layer: "title",
  property: "opacity",
  range: [0, 30],
  from: 0,
  to: 1,
  easing: { type: "spring", stiffness: 200, damping: 12 },
});

Schema Source Map

| File | Schemas | |------|---------| | src/units.ts | PixelSchema, PercentageSchema, UnitValueSchema | | src/coordinates.ts | FrameSchema, BoundsSchema, AnchorPointSchema | | src/color.ts | RGBAColorSchema, HSLAColorSchema, HexColorSchema, ColorSchema | | src/shape.ts | PathPointSchema, RectShapeSchema, EllipseShapeSchema, PathShapeSchema, ShapeSchema, GradientStopSchema, SolidFillSchema, LinearGradientFillSchema, RadialGradientFillSchema, FillSchema, StrokeSchema, TextStyleSchema | | src/easing.ts | LinearEasingSchema, CubicBezierEasingSchema, SpringEasingSchema, StepEasingSchema, EasingPresetSchema, EasingSchema | | src/layer.ts | ShapeVisualSchema, TextVisualSchema, ImageVisualSchema, GroupVisualSchema, RefVisualSchema, VisualSchema, LayerSchema | | src/delta.ts | AnimatablePropertySchema, FrameRangeSchema, DeltaSchema | | src/state.ts | StateSchema | | src/preset.ts | PresetDeltaSchema, PresetSchema | | src/variable.ts | VariableTypeSchema, VariableSchema | | src/asset.ts | AssetTypeSchema, AssetSchema | | src/document.ts | CanvasSchema, AtelierDocumentSchema | | src/validate.ts | validateDocument, validateLayer, validateDelta, ValidationResult, ValidationError | | src/parse.ts | parseAtelier, serializeAtelier |

Animatable Properties

The AnimatablePropertySchema defines every dot-path that can appear in a delta's property field:

frame.x           frame.y
bounds.width       bounds.height
opacity            rotation
scale.x            scale.y
anchorPoint.x      anchorPoint.y
visual.shape.cornerRadius
visual.fill.color  visual.stroke.color  visual.stroke.width
visual.style.fontSize  visual.style.color

Design Decisions

Flat errors over Zod error trees. Zod's native ZodError produces deeply nested issue objects with arrays of unions. The formatErrors() function flattens these into {path, message} pairs that are trivial to iterate, log, or pass to an LLM for interpretation.

Single result type for JSON and YAML. Both validateDocument() and parseAtelier() return ValidationResult<AtelierDocument>, so consuming code handles one shape regardless of input format.

Discriminated unions for extensibility. Shapes, fills, visuals, and easings all use Zod's discriminatedUnion on the type field, matching the tagged-union pattern in @atelier/types. Adding a new visual type means adding one schema variant to the union.

Schemas mirror types, not the other way around. @atelier/types is the source of truth for the type system. Schemas here validate against those types at runtime but do not generate them. This keeps the types package dependency-free.