Introduction
SchemaScript (.scsc) is a structured, block-based schema definition language. You define your data models once – with typed properties, annotations, generics, inheritance, and mapping rules – and a PHP toolchain compiles them into language-specific output: TypeScript interfaces, PHP mapper classes, and more.
Why SchemaScript?
Most projects that communicate across language boundaries end up maintaining parallel type definitions: TypeScript interfaces for the frontend, PHP arrays or DTOs for the backend, maybe a database migration layer on top. These inevitably drift apart. A renamed field in one place becomes a silent bug in another.
SchemaScript eliminates this by making the schema the single source of truth. You describe your data structures in .scsc files, and the toolchain generates type-safe code for each target language – with automatic property name mapping, type casting, and null safety built in.
One schema, many outputs:
┌─────────────┐
│ .scsc file │
└──────┬──────┘
│
▼
┌─────────────┐ ┌────────────────────┐
│ Compiler │────▶│ TypeScript types │ User.ts, _types.ts
└──────┬──────┘ └────────────────────┘
│ ┌────────────────────┐
├───────────▶│ PHP Mappers │ UserMapper.php
│ └────────────────────┘
│ ┌────────────────────┐
└───────────▶│ PHP SAMG Maps │ UserMap.php
└────────────────────┘
Key Features
- Typed properties with integers, floats, strings, booleans, timestamps, UUIDs, and more
- Generics for reusable model templates (
Paginated<T>,Result<T, E>) - Inheritance to compose models from shared base structures
- Property mapping with automatic name conversion between
camelCase,snake_case, and other strategies - Type aliases (public and private) for domain-specific types and string literal unions
- Annotations for language-specific overrides and custom metadata
- Multiple generators producing TypeScript interfaces, PHP mapper classes, and PHP SAMG maps from a single schema
Pipeline
The compilation pipeline transforms .scsc source through several stages:
Source ──▶ Lexer ──▶ Parser ──▶ AST ──▶ Evaluator ──▶ Definition ──▶ Generator
.scsc tokens nodes tree resolved output files
- The Lexer tokenizes source text into a stream of typed tokens
- The Parser builds an abstract syntax tree (AST) from the token stream
- The Evaluator walks the AST, resolves types, imports, and aliases, and produces a
Definition - Generators consume the
Definitionto emit language-specific code
You don’t need to understand the pipeline internals to use SchemaScript – this is just to give you a mental model of how .scsc files become generated code.
What’s Next
- Getting Started walks you through installation and your first schema
- The Language section is a complete reference for the
.scscsyntax - The Toolchain section covers the CLI, build system, and generators
Getting Started
This guide walks you through installing SchemaScript, writing your first schema, and generating code from it.
Requirements
- PHP >= 8.3
- Composer
Installation
Install SchemaScript via Composer:
composer require clancats/schemascript
This adds the scsc CLI tool at vendor/bin/scsc.
Your First Schema
Create a file called SCHEMA.scsc in your project root:
import scsc/base
[generate] = {
[ts.types] = {
output = 'generated/ts/'
}
}
User {
id: int
name: string
email: string?
}
Post {
id: int
title: string
body: string
author: User
tags: string[]
}
This schema defines two models (User and Post) and configures a single generator that will produce TypeScript type definitions.
Building
Run the build command from the directory containing your SCHEMA.scsc:
vendor/bin/scsc build
This generates the following files:
generated/ts/User.ts:
export interface User {
id: number;
name: string;
email: string | null;
}
generated/ts/Post.ts:
import type { User } from './User';
export interface Post {
id: number;
title: string;
body: string;
author: User;
tags: string[];
}
Notice how SchemaScript automatically:
- Mapped
inttonumberandstring?tostring | null - Generated an import statement for the
Userreference inPost - Produced a clean
string[]array type fortags
Adding PHP Mappers
Extend your schema to also generate PHP mapper classes. Update the [generate] block:
import scsc/base
[map] = {
api = {
strategy = MappingStrategy::snakeCase
}
}
[generate] = {
[ts.types] = {
output = 'generated/ts/'
map = api
}
[php.mappers] = {
output = 'generated/php/'
namespace = 'App\\Mappers\\'
map_from = self
map_to = api
}
}
User {
id: int
name: string
email: string?
}
Post {
id: int
title: string
body: string
author: User
tags: string[]
createdAt: int
}
Run vendor/bin/scsc build again. Now you get TypeScript types (with snake_case property names from the api mapping) and PHP mappers that convert between camelCase local keys and snake_case API keys:
generated/php/PostMapper.php (excerpt):
class PostMapper
{
public static function fromArray(array $data): array
{
$result = [];
$result['id'] = (int) $data['id'];
$result['title'] = (string) $data['title'];
$result['body'] = (string) $data['body'];
$result['author'] = UserMapper::fromArray($data['author']);
$result['tags'] = array_map(fn($v) => (string) $v, $data['tags']);
$result['createdAt'] = (int) $data['created_at'];
return $result;
}
}
The mapper automatically:
- Casts each property to its PHP type
- Delegates nested model references to their own mapper
- Converts
created_at(API/snake_case) tocreatedAt(local/camelCase)
Inspecting Your Schema
You can inspect the parsed schema without generating code:
# Print the evaluated Definition as JSON
vendor/bin/scsc SCHEMA.scsc
# Print the raw AST
vendor/bin/scsc ast SCHEMA.scsc
Quick Reference
| Command | Description |
|---|---|
scsc build | Build all generators from SCHEMA.scsc |
scsc gen <name> <file> | Run a specific generator |
scsc <file> | Parse and print as JSON |
scsc ast <file> | Print the AST |
scsc --list-gen | List available generators |
Next Steps
- Syntax Overview for a bird’s-eye view of the language
- Types to learn about the type system
- Models for model definitions, nesting, and visibility
- Property Mapping for name transformation strategies
- Code Generators for detailed generator documentation
Syntax Overview
SchemaScript files use the .scsc extension. The language is block-based and whitespace-insensitive (outside of strings). A schema file contains a mix of top-level directives and model definitions.
Structure of a .scsc File
A typical schema file follows this structure:
// 1. Imports
import scsc/base
// 2. Global metadata
[version] = 1
// 3. Mapping configuration
[map] = {
api = {
strategy = MappingStrategy::snakeCase
}
}
// 4. Generator configuration
[generate] = {
[ts.types] = {
output = 'output/ts/'
}
}
// 5. Constants
const pk_t = uint64
// 6. Namespaces
ns Visibility {
const public
const private
}
// 7. Type aliases
[type] = {
pub MessageType = 'text'|'image'|'video'
position = { x: int, y: int }
}
// 8. Model definitions
User {
id: pk_t
name: string
email: string?
visibility: Visibility::public
}
// 9. Models with generics and inheritance
private SingleResponse<T> {
error?: string
data: T
}
UserResponse: SingleResponse<User> {}
Message {
id: pk_t
type: MessageType
text: string?
author: User?
tags: string[]
metadata: map<string, string>
@map.api(modified_at)
updatedAt: int
}
Top-Level Constructs
| Construct | Syntax | Purpose |
|---|---|---|
| Import | import path/to/file | Include another .scsc file |
| Comment | // text | Line comment |
| Metadata | [key] = value | Schema-level configuration |
| Namespace | ns Name { ... } | Group constants |
| Constant | const name = value | Define a constant |
| Type block | [type] = { ... } | Define type aliases |
| Model | Name { ... } | Define a data model |
Conventions
- File extension:
.scsc - Entry point:
SCHEMA.scscin the project root (used byscsc build) - Standard library:
import scsc/basefor built-in types - No semicolons: Statements are newline-separated
- No commas: Properties and metadata entries are newline-separated (commas are not used)
Imports & Comments
Imports
Use import to include other .scsc files. Path segments are separated by /:
import scsc/base
import common
import models/user
The standard library provides scsc/base, which declares all built-in types (see Standard Library). Most schemas begin with import scsc/base.
Import paths are resolved relative to registered namespace directories. The SchemaScript toolchain resolves imports by scanning registered directories for .scsc files and mapping their file paths to import names. For example, a file at schemas/api/user.scsc registered under the prefix api would be imported as import api/user.
Circular imports are detected and produce an error.
Comments
Line comments use //:
// This is a comment
User {
id: int
}
Multi-Line Comments
Multiple consecutive // lines form a multi-line comment:
// The rotation angle in radians.
// Formula to convert from degrees:
// degrees * (pi / 180)
rotation: float
Comments on Properties
Comments placed directly above a property are attached to that property and preserved through code generation. In TypeScript, they become JSDoc comments. In PHP, they become // comments.
User {
// The user's unique identifier
id: int
// The user's display name
name: string
}
Generated TypeScript (with include_comments = true):
export interface User {
/** The user's unique identifier */
id: number;
/** The user's display name */
name: string;
}
Multi-line comments above a property are preserved as multi-line JSDoc:
Box {
// the rotation in radians
// formula to convert from degrees to radians: degrees * (pi / 180)
rotation: float
}
export interface Box {
/**
* the rotation in radians
* formula to convert from degrees to radians: degrees * (pi / 180)
*/
rotation: number;
}
Metadata
Top-level metadata uses bracket syntax to store schema-level configuration:
[version] = 1
[name] = 'MySchema'
[enabled] = true
Value Types
Metadata values can be:
| Type | Example |
|---|---|
| String | 'hello' or "hello" |
| Number | 42, 3.14 |
| Boolean | true, false |
| Identifier | someIdentifier |
| Namespace reference | MappingStrategy::snakeCase |
| Object | { key = value } |
| List | {'red', 'green', 'blue'} |
Metadata Objects vs Key-Value Objects
There are two different object syntaxes that look similar but produce different data structures.
Metadata Objects
Metadata objects use bracket keys ([key] = value). Each entry is an ordered tuple of key, value, and attributes (annotations). They support:
- Annotations on entries
- Dotted key names
- Duplicate keys (preserved as separate entries)
[generate] = {
[php.mappers] = {
output = 'output/php/Mappers/'
namespace = 'App\\Mappers\\'
}
[ts.types] = {
output = 'output/ts/types/'
}
}
Key-Value Objects
Key-value objects use plain identifiers (key = value). These are simple assignments with no annotation support. Duplicate keys override – only the last value is kept:
[config] = {
name = 'example'
nested = {
deeper = {
value = 42
}
}
}
Mixing Both Forms
Both forms can appear in the same object:
[generate] = {
[php.mappers] = {
output = 'output/php/Mappers/'
namespace = 'App\\Mappers\\'
}
debug = false
}
Metadata Lists
Lists use {value, value} syntax:
[colors] = {'red', 'green', 'blue'}
Metadata with Annotations
Entries within metadata objects can have annotations:
[upgrades] = {
@source('Asgard')
[asgard_core] = 'Complete Asgard knowledge base'
@source('Ancient')
[zpm] = 'Zero Point Module power augmentation'
}
Common Metadata Blocks
[version]
Schema format version:
[version] = 1
[map]
Property mapping configuration (see Property Mapping):
[map] = {
api = {
strategy = MappingStrategy::snakeCase
}
}
[generate]
Generator configuration (see Build System):
[generate] = {
[ts.types] = {
output = 'output/ts/'
}
}
[type]
Type alias definitions (see Type Aliases):
[type] = {
pub MessageType = 'text'|'image'|'video'
}
Model-Level Metadata
Metadata can also appear inside model definitions:
User {
[version] = 2
[map:local] = 'camelCase'
id: int
name: string
}
Types
SchemaScript has a rich type system with simple types, arrays, nullable types, unions, inline objects, string literals, and generics.
Simple Types
A simple type is an identifier referencing a built-in type, a type alias, or a model name:
id: int
name: string
author: User
status: MessageType
String Literal Types
Quoted strings can be used as types, typically combined with unions to constrain a property to specific values:
type: 'text'|'image'|'video'
status: 'active'|'inactive'|'pending'
Arrays
Append [] to a type to make it an array:
colors: int[]
tags: string[]
messages: Message[]
Nullable Types
Append ? to a type to make the value nullable:
name: string?
avatar_url: string?
items: int[]?
string? means the value can be a string or null. int[]? means the value can be an array of ints or null.
Union Types
Combine types with |:
data: Image|User
result: string|int
type: 'text'|'image'|'video'
Inline Objects
Curly braces define anonymous object types with their own properties:
avatar_image: {
url: string
width: int
height: int
}
last_messages: {
cached: bool
data: Message[]
}
Inline objects are expanded inline in generated code – they don’t produce standalone types.
Generic Types
Angle brackets provide type arguments to generic models:
pages: Paginated<User>
config: map<string, string>
nested: map<string, map<string, int>>
See Generics for details on defining and using generic models.
Type Precedence
Type modifiers bind in this order (tightest first):
[](array)?(nullable)|(union)
This means:
| Expression | Meaning |
|---|---|
int[] | Array of int |
int[]? | Nullable array of int |
string|int | String or int |
string|int? | String, or nullable int |
Parentheses
Parentheses override the default precedence:
// Array of nullable int
items: (int?)[]
// Nullable union
value: (string|int)?
// Array of arrays of nullable int
matrix: ((int?)[])[]
Without parentheses, int?[] would mean “nullable array of int”. With parentheses, (int?)[] means “array of nullable int”.
Type Aliases
Type blocks define type aliases and declare custom types using [type] = { ... }:
[type] = {
int64
timestamp = uint64
uuid = string
}
Bare Type Declarations
A bare identifier (no =) registers a type name without aliasing it to another type:
[type] = {
int64
uint64
}
This is primarily used by the standard library to register built-in type names.
Type Aliases
Use = to alias one type to another. Aliases can reference any type expression – simple types, unions, arrays, nullable types, or inline objects:
[type] = {
pk_t = uint64
messageType = 'text'|'image'|'video'
tags = string[]
optionalName = string?
position = {
x: int
y: int
}
}
By default, type aliases are private – generators expand them inline wherever they are used. A property typed as pk_t will appear as bigint in TypeScript and string in PHP (based on the resolved uint64 type), not as a named pk_t type.
Public Type Aliases (pub)
Prefix a type alias with pub to make it public. Public aliases are emitted as standalone, exported types instead of being expanded inline:
[type] = {
pub MessageType = 'text'|'image'|'video'
pub Position = {
x: int
y: int
}
internal_status = 'active'|'inactive' // private, expanded inline
}
TypeScript Output
Public type aliases are collected into a shared _types.ts file. Object-shaped types become export interface, others become export type:
// _types.ts
export type MessageType = 'text' | 'image' | 'video';
export interface Position {
x: number;
y: number;
}
Model files that reference public types automatically import them:
// Message.ts
import type { MessageType } from './_types';
export interface Message {
type: MessageType;
}
PHP Output
Public object-shaped type aliases generate their own mapper class (e.g., PositionMapper.php). Models referencing them delegate to that mapper.
Type Alias Annotations
Type aliases can have annotations, commonly used for language-specific type overrides:
[type] = {
@lang.php('int')
@lang.ts('bigint')
int64
@lang.php('string')
@lang.ts('string')
uuid = string
}
The @lang.php and @lang.ts annotations tell generators what native type to use when emitting code for this type. See Annotations for the full annotation reference.
Scoped Type Blocks
Type blocks can appear inside models. Types defined in a model scope are available to that model and its children, but not to sibling or parent models:
Api {
[type] = {
apiId = string
apiTimestamp = int64
}
Request {
id: apiId
received_at: apiTimestamp
}
Response {
timestamp: apiTimestamp
}
}
// apiId and apiTimestamp are NOT available here
User {
id: int
}
Models
Models are the primary building blocks of a SchemaScript schema. They define named data structures with typed properties.
Basic Syntax
A model is a named block containing property definitions:
User {
id: int
name: string
email: string?
}
Each model produces one output file per generator (e.g., User.ts for TypeScript, UserMapper.php for PHP mappers).
Model-Level Metadata
Models can contain metadata directives that configure behavior specific to that model:
User {
[version] = 2
[map:local] = 'camelCase'
id: int
name: string
}
Child Models
Models can be nested inside other models:
User {
id: int
name: string
Profile {
bio: string
avatar: string?
}
Settings {
theme: string
notifications: bool
}
}
Child models are full models in their own right – they generate their own output files and can be referenced as types by other models.
Private Models
The private keyword prevents a model from being emitted by generators. Private models are useful as base types for inheritance or as generic templates:
private PaginationMeta {
total_count: int
filtered_count: int
}
private SingleResponse<T> {
error?: string
data: T
}
// Public models that inherit from private ones
UserResponse: SingleResponse<User> {}
UserCollectionResponse: PaginationMeta {
error?: string
data: User[]
}
In this example:
PaginationMetaandSingleResponseare not emitted as standalone typesUserResponseinheritsSingleResponse’s properties (withTresolved toUser) and is emittedUserCollectionResponseinheritsPaginationMeta’s properties and is emitted
Generated TypeScript for UserCollectionResponse:
import type { User } from './User';
export interface UserCollectionResponse {
total_count: number;
filtered_count: number;
error?: string;
data: User[];
}
The inherited properties from PaginationMeta are flattened directly into the output.
See Also
- Properties for property syntax and modifiers
- Generics for generic type parameters on models
- Inheritance for extending models from parent types
Properties
Properties define the fields of a model using name: type syntax.
Basic Properties
User {
id: int
name: string
email: string
age: uint8
balance: float
active: bool
}
Nullable Types
Append ? to the type to indicate the value can be null:
User {
id: int
name: string
avatar_url: string? // can be string or null
bio: string? // can be string or null
}
In generated TypeScript, string? becomes string | null. In PHP mappers, nullable properties include a null check before type casting.
Optional Keys
Append ? to the key name (before the colon) to make the key itself optional. An optional key may or may not be present in the data:
Message {
id: int
text: string
unseen?: bool // key may be absent entirely
}
This is distinct from nullable types:
| Syntax | Meaning |
|---|---|
name: string? | Key is always present, value can be null |
name?: string | Key may be absent, value is a string when present |
name?: string? | Key may be absent, value can be null when present |
In generated TypeScript, optional keys use the ? property syntax:
export interface Message {
id: number;
text: string;
unseen?: boolean;
}
In PHP mappers, optional keys are guarded with array_key_exists:
if (array_key_exists('unseen', $data)) {
$result['unseen'] = (bool) $data['unseen'];
}
Property Annotations
Annotations placed directly above a property add metadata to it:
User {
@local(avatarImageId)
avatar_image_id: int?
@enum("text", "image", "video")
type: string
@map.api(modified_at)
updatedAt: int
}
See Annotations for the full annotation reference.
Property Comments
Comments placed directly above a property are attached to it and preserved through code generation:
User {
// The user's unique identifier
id: int
// The user's display name, shown in the UI
name: string
}
See Imports & Comments for details on comment formatting in generated code.
Generics
SchemaScript supports generic type parameters on models and generic type instantiation in property types.
Declaring Generic Models
Models can declare type parameters in angle brackets after the model name:
Paginated<T> {
items: T[]
total: int
}
Result<T, E> {
data: T?
error: E?
}
Type parameters (T, E, etc.) can be used anywhere a type is expected inside the model body – in arrays, nullable types, unions, and inline objects.
Generic Type Instantiation
Use a generic model by providing concrete type arguments in angle brackets:
UserList {
pages: Paginated<User>
outcome: Result<User, string>
}
The number of type arguments must match the number of type parameters declared on the model. Type parameter names must be unique within a model declaration.
Combining with Other Type Modifiers
Generic types follow the same precedence rules as other types and can be combined with arrays, nullable, and unions:
Config {
items: map<string, int>[]
cache: map<string, User>?
nested: map<string, map<string, int>>
}
Child Model Scope
Child models nested inside a generic model can reference the parent’s type parameters:
Container<T> {
value: T
Metadata {
item: T
label: string
}
}
Generic Models and Inheritance
Generic models can be used as parent types. The type parameters are resolved at the inheritance site:
private SingleResponse<T> {
error?: string
data: T
}
private CollectionResponse<T> {
error?: string
data: T[]
}
UserResponse: SingleResponse<User> {}
UserCollectionResponse: CollectionResponse<User> {}
UserResponse inherits SingleResponse’s properties with T resolved to User:
// UserResponse.ts
import type { User } from './User';
export interface UserResponse {
error?: string;
data: User;
}
UserCollectionResponse similarly resolves T to User:
// UserCollectionResponse.ts
import type { User } from './User';
export interface UserCollectionResponse {
error?: string;
data: User[];
}
Standard Library Generic: map
The standard library provides map<K, V>, a key-value mapping type. It generates language-appropriate output:
- TypeScript:
Record<K, V> - PHP:
arraywith value casting viaarray_map()
Settings {
config: map<string, string>
scores: map<string, int>
}
Generated TypeScript:
export interface Settings {
config: Record<string, string>;
scores: Record<string, number>;
}
Generated PHP mapper (excerpt):
$result['config'] = array_map(fn($v) => (string) $v, $data['config']);
$result['scores'] = array_map(fn($v) => (int) $v, $data['scores']);
Custom Generic Models with Language Annotations
To control how a custom generic model is emitted by generators, use @lang.php and @lang.ts annotations. Generic models with these annotations are not emitted as standalone types – instead, generators use the annotation value at each usage site:
@lang.php('array')
@lang.ts('Record')
map<K, V> {}
Generic models without language annotations are emitted as proper generic types in languages that support them. For example, TypeScript emits export interface Paginated<T> { ... }. PHP generators skip generic template models entirely and only handle concrete instantiations through inheritance.
How Generators Handle Generics
| Scenario | TypeScript | PHP |
|---|---|---|
Generic template (Paginated<T>) | Emits interface Paginated<T> | Skips (not emitted) |
Generic with @lang.ts/@lang.php | Uses annotation value (e.g., Record<K, V>) | Uses annotation value |
| Concrete instantiation via inheritance | Resolves type parameters, emits flat interface | Resolves type parameters, emits mapper |
private generic template | Not emitted | Not emitted |
Inheritance
Models can inherit properties from other models using colon syntax.
Basic Inheritance
Use : after the model name to inherit from a parent model:
private PaginationMeta {
total_count: int
filtered_count: int
}
UserCollectionResponse: PaginationMeta {
error?: string
data: User[]
}
The child model (UserCollectionResponse) inherits all properties from the parent (PaginationMeta). Inherited properties appear before the child’s own properties in the generated output:
export interface UserCollectionResponse {
total_count: number; // inherited from PaginationMeta
filtered_count: number; // inherited from PaginationMeta
error?: string; // own property
data: User[]; // own property
}
Properties are flattened – there is no runtime prototype chain or base class in the generated code.
Inheritance with Generics
Parent models can be generic. Type parameters are resolved at the inheritance site:
private SingleResponse<T> {
error?: string
data: T
}
private CollectionResponse<T>: PaginationMeta {
error?: string
data: T[]
}
UserResponse: SingleResponse<User> {}
UserCollectionResponse: CollectionResponse<User> {}
UserResponse resolves T to User, producing:
export interface UserResponse {
error?: string;
data: User;
}
CollectionResponse<T> itself inherits from PaginationMeta, so UserCollectionResponse gets properties from both:
export interface UserCollectionResponse {
total_count: number; // from PaginationMeta (via CollectionResponse)
filtered_count: number; // from PaginationMeta (via CollectionResponse)
error?: string; // from CollectionResponse
data: User[]; // from CollectionResponse, T resolved to User
}
Private Parents
Parent models are often marked private since they serve as templates and shouldn’t produce their own output files. Only the concrete child models are emitted by generators:
private BaseEntity {
id: uint64
created_at: timestamp
updated_at: timestamp
}
User: BaseEntity {
name: string
email: string
}
Post: BaseEntity {
title: string
body: string
}
Both User and Post will include id, created_at, and updated_at in their generated output. BaseEntity itself is not emitted.
Empty Child Models
A child model with no additional properties is valid – it simply takes all properties from its parent:
UserResponse: SingleResponse<User> {}
This is a common pattern for creating concrete types from generic templates.
Constants & Namespaces
Constants are enumeration-style symbols. They can be declared at the root scope or grouped inside namespaces.
Root-Level Constants
const pk_t = uint64
const defaultMapping = MappingType::camelCase
Root-level constants with a type alias value (like pk_t = uint64) can be used as types in property definitions:
const pk_t = uint64
User {
id: pk_t // resolves to uint64
}
Namespaces
Constants grouped inside ns blocks form a namespace:
ns MappingStrategy {
const camelCase
const pascalCase
const snakeCase
const screamingSnakeCase
const kebabCase
}
ns Visibility {
const public
const private
const internal
}
Namespaces can be nested:
ns SCSC {
ns Lang {
ns PHP {
ns Type {
const int = 'int'
const string = 'string'
const bool = 'bool'
}
}
}
}
Referencing Constants
Constants are referenced using :: syntax:
[map] = {
api = {
strategy = MappingStrategy::snakeCase
}
}
Nested namespaces use chained :::
@lang.php(SCSC::Lang::PHP::Type::int)
int64
Constants Without Values
Constants declared without an assigned value resolve to their qualified name as a string. For example, MappingStrategy::camelCase resolves to the string "MappingStrategy::camelCase":
ns Visibility {
const public // resolves to "Visibility::public"
const private // resolves to "Visibility::private"
}
This makes them useful as enum-like symbols in metadata and configuration.
Standard Library Namespaces
The standard library (scsc/base) provides several predefined namespaces:
| Namespace | Constants |
|---|---|
MappingStrategy | camelCase, pascalCase, snakeCase, screamingSnakeCase, kebabCase |
SCSC::Lang::PHP::Type | int, string, bool, float, array, object, null, mixed |
SCSC::Lang::TS::Type | number, string, boolean, any, null, undefined, object, bigint, Uint8Array, unknown |
These are used internally by the standard library’s type annotations and by generators, but you can also reference them in your own schemas.
Annotations
Annotations add metadata to properties, type aliases, and metadata entries using @name or @name(args) syntax.
Syntax
@local(avatarImageId)
avatar_image_id: int?
@enum("text", "image", "video")
type: string
Dotted Names
Annotation names can be dotted for namespacing:
@lang.php('int')
@lang.ts('number')
@map.api(field_name)
Arguments
Arguments are comma-separated and can be strings, numbers, or identifiers:
@name('string argument')
@name("double quoted string")
@name(42)
@name(SomeIdentifier)
@name('first', 'second', 'third')
Where Annotations Can Appear
On Properties
User {
@local(avatarImageId)
avatar_image_id: int?
@map.api(modified_at)
updatedAt: int
}
On Type Aliases
[type] = {
@lang.php('int')
@lang.ts('bigint')
int64
}
On Metadata Entries
[upgrades] = {
@source('Asgard')
[asgard_core] = 'Complete Asgard knowledge base'
}
Known Annotations Reference
| Annotation | Arguments | Purpose |
|---|---|---|
@local(name) | Identifier or string | Override the local property name used in PHP mappers |
@enum(values...) | Strings | Constrain a property to specific string values |
@lang.php(type) | String or namespace ref | Override the PHP type for a type alias |
@lang.ts(type) | String or namespace ref | Override the TypeScript type for a type alias |
@map.<name>(key) | Identifier or string | Override the property key for a specific mapping context |
@local(name)
Explicitly sets the local property name, overriding any automatic naming:
@local(avatarImageId)
avatar_image_id: int?
In PHP mappers, the local key will be avatarImageId regardless of the mapping strategy.
@enum(values...)
Documents the allowed values for a string property:
@enum("text", "image", "video")
type: string
@lang.php(type) and @lang.ts(type)
Override the target language type for a type alias. These are primarily used in the standard library to map SchemaScript types to native types:
[type] = {
@lang.php('int')
@lang.ts('number')
int32
@lang.php('string')
@lang.ts('bigint')
uint64
}
@map.<name>(key)
Override the property key for a specific mapping context:
Message {
@map.api(modified_at)
updatedAt: int
}
When the api mapping is applied, this property will use the key modified_at instead of the automatically converted name. See Property Mapping for details.
Standard Library
The standard library (scsc/base) provides built-in types, mapping strategies, and language-specific type constants. Import it with:
import scsc/base
Built-in Types
Integer Types
| SchemaScript | PHP Type | TypeScript Type |
|---|---|---|
int | int | number |
int8 | int | number |
int16 | int | number |
int32 | int | number |
int64 | int | number |
Unsigned Integer Types
| SchemaScript | PHP Type | TypeScript Type |
|---|---|---|
uint | int | number |
uint8 | int | number |
uint16 | int | number |
uint32 | int | number |
uint64 | string | bigint |
Note:
uint64maps tostringin PHP andbigintin TypeScript because 64-bit unsigned integers exceed the safe integer range of PHP’sintand JavaScript’snumber.
Floating Point Types
| SchemaScript | PHP Type | TypeScript Type |
|---|---|---|
float | float | number |
float32 | float | number |
float64 | float | number |
double | float | number |
Text & Binary
| SchemaScript | PHP Type | TypeScript Type |
|---|---|---|
string | string | string |
bytes | string | Uint8Array |
Boolean
| SchemaScript | PHP Type | TypeScript Type |
|---|---|---|
bool | bool | boolean |
Identifiers
| SchemaScript | PHP Type | TypeScript Type |
|---|---|---|
uuid | string | string |
Temporal Types
| SchemaScript | PHP Type | TypeScript Type |
|---|---|---|
timestamp | string | string |
datetime | string | string |
date | string | string |
time | string | string |
Dynamic Types
| SchemaScript | PHP Type | TypeScript Type |
|---|---|---|
any | mixed | unknown |
mixed | mixed | unknown |
Collections
| SchemaScript | PHP Type | TypeScript Type |
|---|---|---|
map<K, V> | array | Record<K, V> |
map<K, V> is a generic key-value mapping type. See Generics for details.
Mapping Strategies
The MappingStrategy namespace provides constants for property name conversion:
| Constant | Example |
|---|---|
MappingStrategy::camelCase | myProperty |
MappingStrategy::pascalCase | MyProperty |
MappingStrategy::snakeCase | my_property |
MappingStrategy::screamingSnakeCase | MY_PROPERTY |
MappingStrategy::kebabCase | my-property |
Usage:
[map] = {
api = {
strategy = MappingStrategy::snakeCase
}
}
See Property Mapping for details.
Language Type Constants
The standard library also provides namespace constants for native language types. These are used internally by type alias annotations:
PHP types (SCSC::Lang::PHP::Type::*): int, string, bool, float, array, object, null, mixed
TypeScript types (SCSC::Lang::TS::Type::*): number, string, boolean, any, null, undefined, object, bigint, Uint8Array, unknown
You can reference these in your own @lang.php and @lang.ts annotations:
[type] = {
@lang.php(SCSC::Lang::PHP::Type::int)
@lang.ts(SCSC::Lang::TS::Type::number)
myCustomInt
}
CLI Reference
The SchemaScript CLI (scsc) is the main entry point for working with .scsc files. It can parse, evaluate, build, generate, and import schemas.
Usage
vendor/bin/scsc <command> [arguments] [options]
Commands
scsc build
Build all generators configured in the SCHEMA.scsc file in the current working directory.
vendor/bin/scsc build
This is the default command when no arguments are provided. It reads the [generate] metadata block and runs each configured generator, writing output files to the specified directories.
See Build System for details on the SCHEMA.scsc format.
scsc <file>
Parse and evaluate a .scsc file, printing the resulting Definition as JSON:
vendor/bin/scsc schema.scsc
This is useful for inspecting the fully resolved schema – all imports resolved, type aliases expanded, and inheritance flattened.
scsc ast <file>
Print the abstract syntax tree (AST) of a .scsc file as JSON:
vendor/bin/scsc ast schema.scsc
This shows the raw parse tree before evaluation. Useful for debugging parser behavior or understanding how the language is parsed.
scsc gen <generator> <file>
Run a specific generator on a schema file:
vendor/bin/scsc gen ts-types schema.scsc --output=generated/ts/
vendor/bin/scsc gen php-mappers schema.scsc --output=generated/php/
vendor/bin/scsc gen php.samg schema.scsc --output=generated/samg/
Options:
| Option | Description |
|---|---|
--output=<dir> | Output directory for generated files |
--stdout | Print generated code to stdout instead of writing files |
Note: Generator names use hyphens on the CLI (
ts-types,php-mappers) but dots inSCHEMA.scscconfiguration (ts.types,php.mappers).
scsc import <importer> <file>
Import from an external format into SchemaScript:
vendor/bin/scsc import samg SAMG.md --output=schema.scsc
vendor/bin/scsc import samg SAMG.md --stdout
Options:
| Option | Description |
|---|---|
--output=<file> | Output file path for the generated .scsc |
--stdout | Print generated .scsc to stdout |
See Importers for available importers.
scsc --list-gen
List all available generators:
vendor/bin/scsc --list-gen
scsc --list-import
List all available importers:
vendor/bin/scsc --list-import
Examples
# Full build from SCHEMA.scsc
vendor/bin/scsc build
# Inspect the evaluated schema
vendor/bin/scsc SCHEMA.scsc
# Generate only TypeScript types to stdout
vendor/bin/scsc gen ts-types SCHEMA.scsc --stdout
# Import a SAMG file and pipe directly to a generator
vendor/bin/scsc import samg SAMG.md --stdout | vendor/bin/scsc gen php-mappers /dev/stdin --stdout
Build System
The scsc build command orchestrates code generation from a SCHEMA.scsc file. It compiles the schema, validates it, and runs all configured generators.
The SCHEMA.scsc File
By convention, SchemaScript projects use a file called SCHEMA.scsc in the project root. This file combines the schema definitions with generator configuration.
Structure
A typical SCHEMA.scsc follows this structure:
import scsc/base
[version] = 1
[map] = {
api = {
strategy = MappingStrategy::snakeCase
}
}
[generate] = {
[ts.types] = {
output = 'output/ts/types/'
include_comments = true
map = api
}
[php.mappers] = {
output = 'output/php/Mappers/'
namespace = 'App\\Mappers\\'
map_from = self
map_to = api
}
[php.samg] = {
output = 'output/php/SAMG/'
namespace = 'App\\SAMG\\'
map_from = self
map_to = api
}
}
// Type aliases, constants, and model definitions follow...
The [generate] Block
The [generate] metadata block configures which generators to run and their options. Each entry is a metadata object keyed by the generator name:
[generate] = {
[<generator-name>] = {
output = '<output-directory>'
// ... generator-specific options
}
}
Required Options
| Option | Description |
|---|---|
output | Directory where generated files are written (relative to the schema file) |
Common Options
| Option | Description | Used by |
|---|---|---|
namespace | PHP namespace prefix for generated classes | php.mappers, php.samg |
map_from | Source mapping context (default: self) | php.mappers, php.samg |
map_to | Target mapping context | php.mappers, php.samg |
map | Which mapping context to apply for property keys | ts.types |
include_comments | Include doc comments in output | ts.types, php.mappers |
See individual generator pages for the full list of options:
Available Generators
| Name (SCHEMA.scsc) | Name (CLI) | Description |
|---|---|---|
ts.types | ts-types | TypeScript interface definitions |
php.mappers | php-mappers | PHP mapper classes with fromArray/toArray |
php.samg | php.samg | PHP SAMG v2 mapper classes with 10 methods |
Build Process
When you run scsc build, the following happens:
- The
SCHEMA.scscfile is compiled (lexed, parsed, evaluated) with import resolution - The resulting
Definitionis validated - Each entry in the
[generate]block is processed:- The generator is looked up by name
- The generator receives the
Definitionand its configured options - Generated files are written to the specified output directory
- A summary of written files is printed
Full Example
Here is the integration test SCHEMA.scsc as a complete reference:
import scsc/base
[version] = 1
[map] = {
api = {
strategy = MappingStrategy::snakeCase
}
db = {
strategy = MappingStrategy::snakeCase
}
}
[generate] = {
[php.mappers] = {
output = 'output/php/Mappers/'
namespace = 'IntegrationEx\\Mappers\\'
map_from = self
map_to = api
}
[php.samg] = {
output = 'output/php/SAMG/'
namespace = 'IntegrationEx\\SAMG\\'
map_from = self
map_to = api
}
[ts.types] = {
output = 'output/ts/types/'
include_comments = true
map = api
}
}
const pk_t = uint64
private PaginationMeta {
total_count: int
filtered_count: int
}
private SingleResponse<T> {
error?: string
data: T
}
private CollectionResponse<T>: PaginationMeta {
error?: string
data: T[]
}
User {
id: pk_t
name: string
}
UserResponse: SingleResponse<User> {}
UserCollectionResponse: CollectionResponse<User> {}
[type] = {
position = { x: int, y: int }
size = { width: int, height: int }
}
Box {
// the position of the box
pos: position
// the size of the box
size: size
// the rotation in radians
// formula to convert from degrees to radians: degrees * (pi / 180)
rotation: float
}
[type] = {
pub MessageType = 'text'|'image'|'video'
}
Message {
id: pk_t
type: MessageType
text: string?
actor: User?
unseen?: bool
payload: map<string, string>
createdAt: int
@map.api(modified_at)
updatedAt: int
}
Property Mapping
Property mapping transforms property names between different naming conventions. This is how a camelCase property in your schema becomes a snake_case key in a REST API.
Defining Mapping Strategies
The [map] metadata block defines named mapping contexts:
[map] = {
api = {
strategy = MappingStrategy::snakeCase
}
db = {
strategy = MappingStrategy::snakeCase
}
}
Each named context (like api or db) specifies a naming strategy for property keys.
Available Strategies
| Strategy | Example Input | Example Output |
|---|---|---|
MappingStrategy::camelCase | total_count | totalCount |
MappingStrategy::pascalCase | total_count | TotalCount |
MappingStrategy::snakeCase | totalCount | total_count |
MappingStrategy::screamingSnakeCase | totalCount | TOTAL_COUNT |
MappingStrategy::kebabCase | totalCount | total-count |
Using Mappings in Generators
Generators reference mapping contexts through their configuration options:
TypeScript Types
The map option specifies which mapping context to use for property names in the output:
[generate] = {
[ts.types] = {
output = 'output/ts/'
map = api
}
}
With map = api and MappingStrategy::snakeCase, a property createdAt becomes created_at in the TypeScript interface.
PHP Mappers & SAMG
PHP generators use map_from and map_to to define the source and target contexts:
[generate] = {
[php.mappers] = {
output = 'output/php/'
namespace = 'App\\Mappers\\'
map_from = self
map_to = api
}
}
map_from = selfmeans the local/input keys use the original property names from the schemamap_to = apimeans the target/output keys use theapimapping strategy
This generates mappers that convert between camelCase local keys and snake_case API keys:
// fromArray: api -> local
$result['createdAt'] = (int) $data['created_at'];
// toArray: local -> api
$result['created_at'] = (int) $data['createdAt'];
Per-Property Overrides
@map.<name>(key)
Override the mapped key for a specific context:
Message {
createdAt: int
@map.api(modified_at)
updatedAt: int
}
Without the annotation, updatedAt would be automatically converted to updated_at by the snakeCase strategy. The @map.api(modified_at) annotation overrides this, explicitly setting the API key to modified_at.
@local(name)
Override the local property name:
@local(avatarImageId)
avatar_image_id: int?
In PHP mappers, the local key will be avatarImageId regardless of the mapping strategy applied.
Walkthrough Example
Given this schema:
import scsc/base
[map] = {
api = {
strategy = MappingStrategy::snakeCase
}
}
[generate] = {
[ts.types] = {
output = 'output/ts/'
map = api
}
[php.mappers] = {
output = 'output/php/'
namespace = 'App\\Mappers\\'
map_from = self
map_to = api
}
}
Message {
id: uint64
messageText: string
createdAt: int
@map.api(modified_at)
updatedAt: int
}
TypeScript output (using api map = snake_case):
export interface Message {
id: bigint;
message_text: string;
created_at: number;
modified_at: number; // from @map.api annotation
}
PHP mapper (converting between self and api):
public static function fromArray(array $data): array
{
$result = [];
$result['id'] = (string) $data['id'];
$result['messageText'] = (string) $data['message_text'];
$result['createdAt'] = (int) $data['created_at'];
$result['updatedAt'] = (int) $data['modified_at'];
return $result;
}
public static function toArray(array $data): array
{
$result = [];
$result['id'] = (string) $data['id'];
$result['message_text'] = (string) $data['messageText'];
$result['created_at'] = (int) $data['createdAt'];
$result['modified_at'] = (int) $data['updatedAt'];
return $result;
}
Notice how:
messageText(camelCase) maps tomessage_text(snake_case) automaticallyupdatedAtmaps tomodified_at(from the@map.apiannotation, overriding the automaticupdated_at)
Code Generators
Generators take the compiled Definition from a .scsc file and produce language-specific output files. SchemaScript ships with three built-in generators.
Available Generators
| Generator | Output | Description |
|---|---|---|
| TypeScript Types | .ts files | TypeScript interfaces and type aliases |
| PHP Mappers | .php files | PHP classes with fromArray/toArray methods |
| PHP SAMG | .php files | PHP classes with 10 bidirectional mapping methods |
Common Behavior
All generators share these behaviors:
Private Models
Models marked private are not emitted as standalone output files. Their properties are available through inheritance – when a public model inherits from a private one, the inherited properties are flattened into the child’s output.
Generic Template Models
Generic models with unresolved type parameters (e.g., Paginated<T>) are skipped by generators. Only concrete instantiations (via inheritance like UserList: Paginated<User> {}) produce output.
The exception is TypeScript, which can emit generic interfaces directly (e.g., export interface Paginated<T> { ... }) for models without @lang.ts annotations.
Public Type Aliases
Type aliases marked with pub produce standalone output:
- TypeScript: Collected into a shared
_types.tsfile - PHP: Object-shaped public aliases produce their own mapper class
Private type aliases (the default) are expanded inline wherever they are used.
Language Annotations
Generic models with @lang.ts or @lang.php annotations use the annotation value instead of emitting the model. For example, map<K, V> with @lang.ts('Record') emits Record<K, V> in TypeScript instead of generating a map interface.
Configuration
Generators are configured in the [generate] metadata block of a SCHEMA.scsc file:
[generate] = {
[ts.types] = {
output = 'output/ts/'
map = api
include_comments = true
}
[php.mappers] = {
output = 'output/php/Mappers/'
namespace = 'App\\Mappers\\'
map_from = self
map_to = api
}
}
See Build System for details on the configuration format.
Running Generators
Generators run automatically with scsc build, or individually with scsc gen:
# Run all generators
vendor/bin/scsc build
# Run a specific generator
vendor/bin/scsc gen ts-types schema.scsc --output=output/ts/
vendor/bin/scsc gen php-mappers schema.scsc --output=output/php/
List available generators:
vendor/bin/scsc --list-gen
TypeScript Types Generator
The ts.types generator produces TypeScript interface definitions and type aliases from SchemaScript models.
Configuration
[generate] = {
[ts.types] = {
output = 'output/ts/types/'
map = api
include_comments = true
}
}
Options
| Option | Required | Description |
|---|---|---|
output | Yes | Output directory for generated .ts files |
map | No | Mapping context to apply for property names |
include_comments | No | Include JSDoc comments from schema comments |
CLI name: ts-types
vendor/bin/scsc gen ts-types schema.scsc --output=output/ts/
Output Files
The generator produces:
- One
.tsfile per model named after the model (e.g.,User.ts,Message.ts) _types.tsif there are public type aliases – contains allpubtype aliases
Examples
Simple Model
User {
id: uint64
name: string
}
User.ts:
export interface User {
id: bigint;
name: string;
}
Model with References and Nullable Types
Message {
id: uint64
type: MessageType
text: string?
actor: User?
unseen?: bool
payload: map<string, string>
created_at: int
modified_at: int
}
Message.ts:
import type { MessageType } from './_types';
import type { User } from './User';
export interface Message {
id: bigint;
type: MessageType;
text: string | null;
actor: User | null;
unseen?: boolean;
payload: Record<string, string>;
created_at: number;
modified_at: number;
}
Public Type Aliases
[type] = {
pub MessageType = 'text'|'image'|'video'
}
_types.ts:
export type MessageType = 'text' | 'image' | 'video';
Inline Objects with Comments
Box {
// the position of the box
pos: position
// the size of the box
size: size
// the rotation in radians
// formula to convert from degrees to radians: degrees * (pi / 180)
rotation: float
}
Where position = { x: int, y: int } and size = { width: int, height: int } are private type aliases:
Box.ts:
export interface Box {
/** the position of the box */
pos: {
x: number;
y: number;
};
/** the size of the box */
size: {
width: number;
height: number;
};
/**
* the rotation in radians
* formula to convert from degrees to radians: degrees * (pi / 180)
*/
rotation: number;
}
Inheritance with Generics
private SingleResponse<T> {
error?: string
data: T
}
UserResponse: SingleResponse<User> {}
UserResponse.ts:
import type { User } from './User';
export interface UserResponse {
error?: string;
data: User;
}
Inherited Properties from Multiple Parents
private PaginationMeta {
total_count: int
filtered_count: int
}
private CollectionResponse<T>: PaginationMeta {
error?: string
data: T[]
}
UserCollectionResponse: CollectionResponse<User> {}
UserCollectionResponse.ts:
import type { User } from './User';
export interface UserCollectionResponse {
total_count: number;
filtered_count: number;
error?: string;
data: User[];
}
Type Mapping Reference
| SchemaScript | TypeScript |
|---|---|
int, int8…int64 | number |
uint…uint32 | number |
uint64 | bigint |
float, double | number |
string | string |
bool | boolean |
bytes | Uint8Array |
uuid, timestamp, datetime, date, time | string |
any, mixed | unknown |
T[] | T[] |
T? | T | null |
map<K, V> | Record<K, V> |
'a'|'b'|'c' | 'a' | 'b' | 'c' |
{ x: int } | { x: number } |
| Model reference | Import + type name |
PHP Mappers Generator
The php.mappers generator produces PHP mapper classes with fromArray and toArray static methods for bidirectional data transformation.
Configuration
[generate] = {
[php.mappers] = {
output = 'output/php/Mappers/'
namespace = 'App\\Mappers\\'
map_from = self
map_to = api
}
}
Options
| Option | Required | Description |
|---|---|---|
output | Yes | Output directory for generated .php files |
namespace | No | PHP namespace prefix for generated classes |
map_from | No | Source mapping context (default: self) |
map_to | No | Target mapping context |
include_comments | No | Include comments in generated code |
CLI name: php-mappers
vendor/bin/scsc gen php-mappers schema.scsc --output=output/php/
Output Files
One <Model>Mapper.php file per public model. Each class has two static methods:
fromArray(array $data): array– Converts from themap_tocontext to themap_fromcontext (e.g., API data to local format)toArray(array $data): array– Converts from themap_fromcontext to themap_tocontext (e.g., local format to API data)
Features
Type Casting
Each property is automatically cast to its PHP type:
$result['id'] = (string) $data['id']; // uint64 -> string
$result['name'] = (string) $data['name']; // string -> string
$result['age'] = (int) $data['age']; // int -> int
$result['score'] = (float) $data['score']; // float -> float
$result['active'] = (bool) $data['active']; // bool -> bool
Nullable Properties
Nullable types include a null check:
$result['text'] = ($data['text'] !== null ? (string) $data['text'] : null);
$result['actor'] = ($data['actor'] !== null ? UserMapper::fromArray($data['actor']) : null);
Optional Keys
Optional keys (name?:) are guarded with array_key_exists:
if (array_key_exists('unseen', $data)) {
$result['unseen'] = (bool) $data['unseen'];
}
Nested Model References
Properties referencing other models delegate to their mapper:
$result['actor'] = UserMapper::fromArray($data['actor']);
$result['actor'] = ($data['actor'] !== null ? UserMapper::fromArray($data['actor']) : null);
Map Types
map<K, V> properties use array_map() for value casting:
$result['payload'] = array_map(fn($v) => (string) $v, $data['payload']);
$result['scores'] = array_map(fn($v) => (int) $v, $data['scores']);
Property Name Mapping
When map_from and map_to differ, property keys are converted:
// fromArray: api keys -> local keys
$result['createdAt'] = (int) $data['created_at'];
// toArray: local keys -> api keys
$result['created_at'] = (int) $data['createdAt'];
Full Example
Given this schema:
Message {
id: uint64
type: MessageType
text: string?
actor: User?
unseen?: bool
payload: map<string, string>
createdAt: int
@map.api(modified_at)
updatedAt: int
}
MessageMapper.php:
<?php
namespace IntegrationEx\Mappers;
class MessageMapper
{
public static function fromArray(array $data): array
{
$result = [];
$result['id'] = (string) $data['id'];
$result['type'] = $data['type'];
$result['text'] = ($data['text'] !== null ? (string) $data['text'] : null);
$result['actor'] = ($data['actor'] !== null ? UserMapper::fromArray($data['actor']) : null);
if (array_key_exists('unseen', $data)) {
$result['unseen'] = (bool) $data['unseen'];
}
$result['payload'] = array_map(fn($v) => (string) $v, $data['payload']);
$result['createdAt'] = (int) $data['created_at'];
$result['updatedAt'] = (int) $data['modified_at'];
return $result;
}
public static function toArray(array $data): array
{
$result = [];
$result['id'] = (string) $data['id'];
$result['type'] = $data['type'];
$result['text'] = ($data['text'] !== null ? (string) $data['text'] : null);
$result['actor'] = ($data['actor'] !== null ? UserMapper::toArray($data['actor']) : null);
if (array_key_exists('unseen', $data)) {
$result['unseen'] = (bool) $data['unseen'];
}
$result['payload'] = array_map(fn($v) => (string) $v, $data['payload']);
$result['created_at'] = (int) $data['createdAt'];
$result['modified_at'] = (int) $data['updatedAt'];
return $result;
}
}
PHP SAMG Generator
The php.samg generator produces PHP SAMG (Schema Auto-Mapping Generation) v2 mapper classes with 10 static methods for bidirectional property mapping and type casting.
Configuration
[generate] = {
[php.samg] = {
output = 'output/php/SAMG/'
namespace = 'App\\SAMG\\'
map_from = self
map_to = api
}
}
Options
| Option | Required | Description |
|---|---|---|
output | Yes | Output directory for generated .php files |
namespace | No | PHP namespace prefix for generated classes |
map_from | No | Source mapping context (default: self) |
map_to | No | Target mapping context |
CLI name: php.samg
vendor/bin/scsc gen php.samg schema.scsc --output=output/samg/
Output Files
One <Model>Map.php file per public model. Each class has 10 static methods organized into three categories: full mapping, partial mapping, and in-place casting.
The 10 Methods
Full Mapping (4 methods)
These methods map all properties, using ?? null for missing keys:
| Method | Direction | Type Casting |
|---|---|---|
localToInterface(array $array): array | Local -> Interface | Yes |
localToInterfaceMapOnly(array $array): array | Local -> Interface | No |
interfaceToLocal(array $array): array | Interface -> Local | Yes |
interfaceToLocalMapOnly(array $array): array | Interface -> Local | No |
“MapOnly” variants only transform property keys without casting values. Useful when the data is already correctly typed and you only need name conversion.
Partial Mapping (4 methods)
These methods only map properties that exist in the input array, using array_key_exists guards. Ideal for PATCH-style updates:
| Method | Direction | Type Casting |
|---|---|---|
localToPartialInterface(array $array): array | Local -> Interface | Yes |
localToPartialInterfaceMapOnly(array $array): array | Local -> Interface | No |
interfaceToPartialLocal(array $array): array | Interface -> Local | Yes |
interfaceToPartialLocalMapOnly(array $array): array | Interface -> Local | No |
In-Place Casting (2 methods)
These methods cast property values in-place by reference, without creating a new array or changing property keys:
| Method | Context |
|---|---|
castLocal(array &$array): void | Cast values using local keys |
castInterface(array &$array): void | Cast values using interface keys |
Example
Given this schema:
User {
id: uint64
name: string
}
UserMap.php:
<?php
namespace IntegrationEx\SAMG;
class UserMap
{
/**
* @param array<mixed> $array
* @return array<mixed>
*/
public static function localToInterface(array $array): array
{
return [
'id' => (string) ($array['id'] ?? null),
'name' => (string) ($array['name'] ?? null),
];
}
/**
* @param array<mixed> $array
* @return array<mixed>
*/
public static function localToInterfaceMapOnly(array $array): array
{
return [
'id' => $array['id'] ?? null,
'name' => $array['name'] ?? null,
];
}
/**
* @param array<mixed> $array
* @return array<mixed>
*/
public static function interfaceToLocal(array $array): array
{
return [
'id' => (string) ($array['id'] ?? null),
'name' => (string) ($array['name'] ?? null),
];
}
/**
* @param array<mixed> $array
* @return array<mixed>
*/
public static function interfaceToLocalMapOnly(array $array): array
{
return [
'id' => $array['id'] ?? null,
'name' => $array['name'] ?? null,
];
}
/**
* @param array<mixed> $array
* @return array<mixed>
*/
public static function localToPartialInterface(array $array): array
{
$buffer = [];
if (array_key_exists('id', $array)) {
$buffer['id'] = (string) ($array['id'] ?? null);
}
if (array_key_exists('name', $array)) {
$buffer['name'] = (string) ($array['name'] ?? null);
}
return $buffer;
}
/**
* @param array<mixed> $array
* @return array<mixed>
*/
public static function localToPartialInterfaceMapOnly(array $array): array
{
$buffer = [];
if (array_key_exists('id', $array)) {
$buffer['id'] = $array['id'];
}
if (array_key_exists('name', $array)) {
$buffer['name'] = $array['name'];
}
return $buffer;
}
/**
* @param array<mixed> $array
* @return array<mixed>
*/
public static function interfaceToPartialLocal(array $array): array
{
$buffer = [];
if (array_key_exists('id', $array)) {
$buffer['id'] = (string) ($array['id'] ?? null);
}
if (array_key_exists('name', $array)) {
$buffer['name'] = (string) ($array['name'] ?? null);
}
return $buffer;
}
/**
* @param array<mixed> $array
* @return array<mixed>
*/
public static function interfaceToPartialLocalMapOnly(array $array): array
{
$buffer = [];
if (array_key_exists('id', $array)) {
$buffer['id'] = $array['id'];
}
if (array_key_exists('name', $array)) {
$buffer['name'] = $array['name'];
}
return $buffer;
}
/**
* @param array<mixed> $array
*/
public static function castLocal(array &$array): void
{
$array['id'] = (string) ($array['id'] ?? null);
$array['name'] = (string) ($array['name'] ?? null);
}
/**
* @param array<mixed> $array
*/
public static function castInterface(array &$array): void
{
$array['id'] = (string) ($array['id'] ?? null);
$array['name'] = (string) ($array['name'] ?? null);
}
}
When to Use Which Method
| Use Case | Method |
|---|---|
| API response -> local storage | interfaceToLocal |
| Local data -> API request | localToInterface |
| PATCH update (only changed fields) | localToPartialInterface / interfaceToPartialLocal |
| Key rename without casting (pre-casted data) | *MapOnly variants |
| Normalize types on existing array | castLocal / castInterface |
Importers
Importers convert external format definitions into SchemaScript .scsc files. This lets you adopt SchemaScript incrementally by importing existing schema definitions.
Usage
vendor/bin/scsc import <importer> <file> [--output=<file.scsc>] [--stdout]
Options
| Option | Description |
|---|---|
--output=<file> | Write the generated .scsc to a file |
--stdout | Print the generated .scsc to stdout |
List available importers:
vendor/bin/scsc --list-import
Available Importers
SAMG (samg)
The SAMG importer reads SAMG (Storage Abstraction Mapping Graph) markdown definition files and converts them into SchemaScript schemas.
vendor/bin/scsc import samg SAMG.md --output=schema.scsc
The importer:
- Parses model definitions and their properties from the SAMG markdown format
- Generates appropriate
[map]and[generate]blocks - Configures
snake_casemapping strategy for the API context - Sets up
php.samggenerator configuration with the namespace and output path from the SAMG metadata
Piping to a Generator
You can pipe imported output directly to a generator:
vendor/bin/scsc import samg SAMG.md --stdout | vendor/bin/scsc gen php.samg /dev/stdin --output=output/
This is useful for one-off generation without creating an intermediate .scsc file.