• SPECML
    • A-SPECML

    SPECML

    Syntax Fundamentals#

    Line-Based Structure#

    SPECML schemas are line-based. Each line contains a declaration and optionally an expression:
    declaration expression
    The lexer splits each line at the first contiguous block of whitespace:
    Everything before the first whitespace block is the declaration
    Everything after is the expression
    Both spaces and tabs count as whitespace. Multiple consecutive spaces or tabs are treated as a single separator.
    age string                    // Valid
    age                  string   // Same as above
    age	string                     // Tab separator - also valid

    Blank Lines#

    Blank lines (including lines with only whitespace) are allowed and ignored. Use them to organize your schema for readability.
    User {
      id string
      email string
    
      createdAt string
    }

    Comments#

    Comments use // syntax. Everything after // is ignored by the parser.
    Comments can appear:
    At the end of a line with a declaration
    On their own line
    With leading whitespace
    // This is a standalone comment
    age string // Age of the user
    age:HumanAge number // Age between 3 and 120
    
        // Comment with leading spaces
    status string(active|inactive) // User account status
    
    // Multiple standalone comments
    // Can appear consecutively
    User {
      id string
    }

    Quoted Strings#

    Quotes (single ' or double ") serve two purposes in SPECML:
    1. Grouping tokens with special characters
    When declarations or values contain spaces or special characters, wrap them in quotes to treat them as a single token:
    "Account Name" string
    'User Email' string<isEmail>
    "field-with-dashes" number
    Without quotes, Account Name would be split at the space, treating Account as the declaration and Name as the expression.
    2. Escaping SPECML syntax
    Use quotes to treat SPECML special characters (like #, :, [], ?) as literal characters rather than syntax:
    person#Human              // References the Human type
    "person#Human" string     // Literal field name containing #
    
    field:output string       // Creates an alias
    "field:output" string     // Literal field name containing :
    Quote matching rules:
    Opening and closing quotes must match ("..." or '...')
    Mix quote types inside: "Account 'Statement'" or 'Account "Statement"'
    Escape quotes with backslash: "Account \"Escaped\""
    Quote awareness:
    The lexer and parser are quote-aware throughout SPECML. Quotes work in declarations, expressions, modifier values, and enum values:
    "First Name" string<minLength:3>           // Quoted declaration
    pattern string<matches:"^[A-Z]{2,5}$">    // Quoted in modifier value
    mode string(read|write|"read|write")      // Quoted in enum value

    Whitespace Flexibility#

    SPECML is flexible with whitespace within expressions. You can use spaces for readability:
    age number<min:10|max:100>           // Compact
    age number < min:10 | max:100 >      // Spaced out
    age number  <  min:10  |  max:100  > // Very spaced
    
    status string(active|inactive)       // Compact
    status string ( active | inactive )  // Spaced
    
    tags[] string                        // Compact
    tags [] string                       // Spaced
    tags [ ] string                      // More spaced
    All variations above are valid and equivalent.
    Constraint: Each declaration must be on a single line. Multi-line declarations are not supported.

    Empty Declarations#

    A declaration without an expression is valid and implicitly accepts any type of value (object, string, number, boolean, null, etc.):
    metadata           // Can be any type
    config             // Can be any type
    data               // Can be any type
    This is useful for defining flexible fields where the type is not constrained.

    Examples#

    Basic Schema with Comments#

    // User authentication schema
    // This schema defines the core user model
    
    User {
      id string<ulid>                      // Unique identifier
      email string<isEmail|lowercase>      // Must be valid email
      
      // Username constraints
      username string<minLength:3>         // At least 3 characters
      
      age number<min:13>                   // Minimum age requirement
      status string(active|inactive)       // Account status
    }
    
    // End of User schema

    Using Quoted Strings#

    APIResponse {
      "Content-Type" string                // HTTP header with dash
      "X-Request-ID" string<ulid>          // Header with special chars
      "user#id" string                     // Field name with # character
      "created:at" string<isISO>           // Field name with : character
    }

    Flexible Whitespace#

    Product {
      price number<min:0|max:1000000>
      
      // Same as above, just more spaced for readability
      discountPrice number < min:0 | max:1000000 >
      
      // Arrays with spacing
      tags[] string
      categories [ ] string
      
      // Enums with spacing
      status string(active|inactive|archived)
      priority string ( low | medium | high )
    }

    Escaping SPECML Syntax#

    // These fields have names that look like SPECML syntax
    Config {
      "field#ref" string           // # is literal, not a reference
      "input:output" string        // : is literal, not an alias
      "items[]" string             // [] is literal, not an array
      "optional?" boolean          // ? is literal, not optional marker
    }
    
    
    ## Basic Declarations
    
    ### Simple Names
    
    The `baseName` is the foundation of every declaration. At its simplest, it's an identifier for your field or type.
    
    ```specml
    name
    email
    age
    firstName
    user_id

    Allowed Characters#

    Without quotes, baseNames can contain:
    Letters (a-z, A-Z)
    Numbers (0-9)
    Underscore _
    Hyphen -
    Hash #
    Dollar sign `## Basic Declarations

    Simple Names#

    The baseName is the foundation of every declaration. At its simplest, it's an identifier for your field or type.
    name
    email
    age
    firstName
    user_id
    At symbol @
    Period .
    BaseNames can start with any of these characters, including numbers and hyphens:
    firstName
    user_id
    user-name
    -field
    --
    field.name
    user@domain
    $price
    #tag
    123account
    9total
    Rule of thumb: If it's valid as a property name in a JavaScript object (the left side of { "key": value }), it's valid as a baseName in SPECML.
    With quotes, baseNames can contain any characters, including spaces and special symbols:
    "First Name"
    "User Email"
    "field-with-special-chars!"
    "user@domain.com"
    "price (USD)"
    "field/path"

    When to Use Quotes#

    Use quotes when your baseName contains:
    1.
    Spaces
    2.
    Special characters that would confuse the parser (:, [, ], ?, {, })
    3.
    Unicode characters (emoji, non-Latin scripts)
    // Requires quotes - contains spaces
    "Account Name" string
    "First Name" string
    
    // Requires quotes - contains parser syntax characters
    "field:output" string     // Without quotes, : would be interpreted as alias syntax
    "items[]" string          // Without quotes, [] would be interpreted as array syntax
    "optional?" string        // Without quotes, ? would be interpreted as optional syntax
    "object{}" string         // Without quotes, {} would be interpreted as block syntax
    
    // Requires quotes - unicode characters
    "用户名" string           // Chinese characters
    "prénom" string           // Accented characters
    "👤 User" string          // Emoji

    Character Context#

    Some characters like #, ., and @ are allowed in baseNames without quotes and have no special meaning when used this way:
    // These are just literal characters in the baseName
    field.name string
    user@domain string
    element#id string
    $amount number
    Note on periods: While field.name is a valid baseName in SPECML, it does NOT represent nested object notation. It's simply a field named field.name. This is different from some other schema languages where dots indicate nesting.
    However, when # is used as part of SPECML syntax (for references), it must be outside quotes:
    owner#Person              // References Person type
    "owner#Person" string     // Literal field name containing #

    Case Sensitivity#

    BaseNames are case-sensitive. UserName and username are distinct fields:
    User {
      UserName string    // Different from username
      username string    // Different from UserName
      EMAIL string       // Different from email
      email string       // Different from EMAIL
    }

    Numeric Names#

    BaseNames can be purely numeric or start with numbers:
    123 string
    456 number
    789account string
    9total number
    This is valid but purely numeric names are generally not recommended for readability.

    Reserved Words#

    SPECML has no reserved words. You can use any identifier as a baseName:
    true boolean
    false boolean
    null string
    root object
    type string
    All of these are valid baseNames.

    Quote Matching Rules#

    When using quotes:
    Opening and closing quotes must match: "..." or '...'
    You can mix quote types inside: "Account 'Name'" or 'Account "Name"'
    Escape quotes with backslash: "Account \"Name\""
    "Account Name" string              // Double quotes
    'Account Name' string              // Single quotes
    "Account 'Primary' Name" string    // Mixed quotes
    'Account "Primary" Name' string    // Mixed quotes
    "Account \"Escaped\" Name" string  // Escaped quotes

    Special Characters Reference#

    CharacterWithout QuotesWith QuotesNotes
    Space❌ Invalid✅ ValidCauses lexer split
    _✅ Valid✅ ValidCommon in identifiers
    -✅ Valid✅ ValidCommon in kebab-case
    .✅ Valid✅ ValidNo special meaning in baseName
    #✅ Valid✅ ValidOnly special when used as reference syntax
    $✅ Valid✅ ValidCommon in variable names
    @✅ Valid✅ ValidNo special meaning
    :❌ Invalid✅ ValidUsed for alias syntax
    [ ]❌ Invalid✅ ValidUsed for array syntax
    ?❌ Invalid✅ ValidUsed for optional syntax
    { }❌ Invalid✅ ValidUsed for block syntax
    /❌ Invalid✅ ValidCould confuse parser
    \❌ Invalid✅ ValidUsed for escaping

    Examples#

    Simple Identifiers#

    User {
      id string
      email string
      age number
      firstName string
      lastName string
      isActive boolean
    }

    With Underscores and Hyphens#

    Product {
      product_id string
      product-name string
      unit_price number
      in-stock boolean
      created_at string
      updated-at string
      -priority number
      -- string
      _internal string
    }

    With Dots and Special Characters#

    Config {
      api.endpoint string
      api.key string
      user@domain string
      $price number
      #identifier string
    }

    Requiring Quotes#

    APIResponse {
      "Content-Type" string
      "X-Request-ID" string
      "Cache-Control" string
    }
    
    Invoice {
      "Line Item 1" string
      "Line Item 2" string
      "Total (USD)" number
      "Tax Rate %" number
    }
    
    Multilingual {
      "用户名" string        // Chinese
      "nom d'utilisateur" string  // French
      "Benutzername" string // German
      "👤 Profile" string   // Emoji
    }

    Escaping in BaseNames#

    Schema {
      // Using SPECML syntax as literal characters
      "field:alias" string          // : is literal
      "array[]" string              // [] is literal
      "optional?" string            // ? is literal
      "reference#type" string       // # is literal
      "object{}" string             // {} is literal
    }

    Numeric Identifiers#

    ErrorCodes {
      100 string
      200 string
      404 string
      500 string
    }

    Case Sensitivity Examples#

    User {
      ID string       // Different from id
      id string       // Different from ID
      Email string    // Different from email
      email string    // Different from Email
    }

    Mixed Patterns#

    Company {
      // Simple names
      name string
      industry string
      
      // With underscores
      employee_count number
      
      // With hyphens
      tax-id string
      
      // With dots
      domain.name string
      
      // Requiring quotes
      "Legal Name" string
      "Founded (Year)" number
      "# of Offices" number
    }

    Key Takeaways#

    1.
    Simple names don't need quotes and can use letters, numbers, _, -, ., #, $, @
    2.
    Use quotes for spaces, parser syntax characters (:, [], ?, {}), and unicode
    3.
    Case matters - UserName ≠ username
    4.
    No reserved words - any identifier is valid
    5.
    Numeric names are allowed but not recommended
    6.
    Quote types can be mixed or matched as needed
    For common mistakes with declarations, see Appendix: Common Pitfalls.

    References with ##

    The # symbol creates a reference to another type definition, enabling type reuse and composition throughout your schema.

    Basic References#

    Reference another type using #TypeName after the baseName:
    Person {
      name string<minLength:1>
      age number<min:18>
      email string<isEmail>
    }
    
    Employee {
      employeeId string
      person#Person
    }
    The person field in Employee references the Person type, inheriting all its properties and constraints.
    Result: When validated or processed, person will have name, age, and email fields with all their constraints.

    Type Resolution#

    References are resolved within the same file. SPECML looks for type definitions in the current schema file only.
    // All types must be defined in the same file
    Address {
      street string
      city string
    }
    
    User {
      homeAddress#Address  // References Address defined in this file
    }
    Note: Cross-file references are not currently supported.

    Forward References#

    You can reference a type before it's defined in the file. The parser has context of all defined types, so order doesn't technically matter:
    User {
      address#Address  // Address is defined later - this is valid
    }
    
    Address {
      street string
      city string
      zipCode string
    }
    However, for readability and maintainability, it's recommended to define types before referencing them:
    // Recommended: Define first, reference later
    Address {
      street string
      city string
      zipCode string
    }
    
    User {
      address#Address
    }

    Chained References#

    Reference nested types using multiple # symbols. Resolution is hierarchical:
    Company {
      Locations {
        Headquarters {
          city string
          country string
        }
      }
    }
    
    Branch {
      location#Company#Locations#Headquarters
    }
    How it works:
    1.
    SPECML finds Company
    2.
    Within Company, it finds Locations
    3.
    Within Locations, it finds Headquarters
    4.
    The location field inherits all properties from Headquarters
    Chain depth: You can chain as deeply as needed: #Type1#Type2#Type3#Type4...
    Error handling: If any type in the chain doesn't exist or doesn't contain the next type, SPECML throws an error:
    // Error: Type2 not found in Type1
    field#Type1#Type2

    Using # as Literal Character#

    To use # as a regular character in a baseName without reference meaning, enclose it in quotes:
    "currentAccount#number" string  // Just a property name, not a reference
    "field#123" number              // # is literal
    "user#id" string                // # is literal

    Complex Reference Patterns#

    You can combine references with other declaration components following the ordering rule: baseName#reference:alias[length]?
    Address {
      street string
      city string
    }
    
    User {
      // Reference only
      homeAddress#Address
      
      // Reference + optional
      workAddress#Address?
      
      // Reference + array
      previousAddresses#Address[]
      
      // Reference + array + optional
      vacationHomes#Address[]?
      
      // Reference + alias
      primary#Address:primaryAddress
      
      // Reference + alias + array + optional
      contacts#Address:mailingAddresses[]?
    }

    References with Inline Blocks#

    When you reference a type and include a block {}, you're applying limited modifications to that reference, not extending it:
    Person {
      name string<minLength:1>
      age number<min:18>
      title string(mr|mrs|miss|dr)
      nickname? string
    }
    
    Employee {
      person#Person {
        title?        // Make required field optional
        nickname      // Make optional field required
      }
    }
    Important: You cannot add new fields, modify constraints, change types, or use references within the modification block. See Reference Constraints below.

    Circular Reference Prevention#

    SPECML does not allow circular references. If Type A references Type B, and Type B references Type A, an error is thrown:
    // INVALID - Circular reference
    TypeA {
      field#TypeB
    }
    
    TypeB {
      field#TypeA  // Error: Circular reference detected
    }

    Self-Reference Prevention#

    Types cannot reference themselves:
    // INVALID - Self-reference
    TreeNode {
      value string
      children#TreeNode[]  // Error: Self-reference not allowed
    }
    For recursive structures, you'll need to handle them outside of SPECML's type system.

    Reference Constraints#

    References in SPECML follow strict rules to maintain schema integrity. You have limited ability to modify referenced types.

    What You CAN Modify#

    1. Optionality (Bidirectional)#

    Toggle whether fields are required or optional:
    Person {
      name string
      age number
      bio? string
    }
    
    User {
      profile#Person {
        age?    // Make required field optional
        bio     // Make optional field required
      }
    }
    Result in User:
    name remains required
    age becomes optional (was required)
    bio becomes required (was optional)

    2. Enums (Narrowing Only)#

    Restrict enum values to a subset of the original:
    Person {
      title string(mr|mrs|miss|dr|prof)
      status string(active|inactive|pending)
    }
    
    Customer {
      contact#Person {
        title string(mr|mrs|miss)  // Subset of original enum
      }
    }
    Valid: Using fewer values from the original enum.
    Invalid: Adding new values or using values not in the original enum.

    What You CANNOT Modify#

    1. Modifiers (Completely Immutable)#

    You cannot change, add, or remove modifiers:
    Person {
      name string<minLength:1>
      age number<min:18|max:120>
    }
    
    // ALL OF THESE ARE INVALID:
    
    User {
      profile#Person {
        age number<min:25>           // ❌ Cannot change modifier values
        age number<min:18|max:65>    // ❌ Cannot add new modifiers
        name string                  // ❌ Cannot remove modifiers
      }
    }

    2. Fields (No Addition or Removal)#

    You cannot add new fields or remove existing ones:
    Person {
      name string
      age number
    }
    
    // INVALID:
    
    User {
      profile#Person {
        email string    // ❌ Cannot add new fields
      }
    }
    You also cannot selectively omit fields - all fields from the referenced type are inherited.

    3. Field Types#

    You cannot change the type of a field:
    Person {
      age number
      name string
    }
    
    // INVALID:
    
    User {
      profile#Person {
        age string    // ❌ Cannot change type from number to string
      }
    }

    4. References Within Modifications#

    You cannot use references inside reference modification blocks:
    Address {
      street string
      city string
    }
    
    Person {
      name string
      homeAddress#Address
    }
    
    // INVALID:
    
    User {
      contact#Person {
        workAddress#Address    // ❌ Cannot add references in modification block
      }
    }

    Valid Reference Modifications#

    Here's a complete example showing all allowed modifications:
    Person {
      firstName string<minLength:1>
      lastName string<minLength:1>
      age number<min:18|max:120>
      title string(mr|mrs|miss|dr|prof)
      nickname? string
      bio? string<maxLength:500>
    }
    
    // Using Person as-is
    Employee {
      contact#Person
    }
    
    // Modifying optionality and narrowing enums
    Customer {
      primaryContact#Person {
        age?                          // Make required optional
        nickname                      // Make optional required
        title string(mr|mrs|miss)     // Narrow enum to subset
      }
    }

    Invalid Reference Modifications#

    Person {
      name string<minLength:1>
      age number<min:18>
      title string(mr|mrs|miss)
    }
    
    // ALL INVALID:
    
    User {
      profile#Person {
        email string                      // ❌ Cannot add fields
        age number<min:25>                // ❌ Cannot modify constraints
        name string                       // ❌ Cannot remove modifiers
        title string(mr|mrs|miss|dr)      // ❌ Cannot extend enum
        age string                        // ❌ Cannot change type
      }
    }

    Examples#

    Basic Type Reuse#

    Timestamps {
      createdAt string<isISO>
      updatedAt string<isISO>
    }
    
    User {
      id string<ulid>
      email string<isEmail>
      timestamps#Timestamps
    }
    
    Article {
      id string<ulid>
      title string
      content string
      timestamps#Timestamps
    }

    Nested References#

    Address {
      street string
      city string
      zipCode string
    }
    
    ContactInfo {
      email string<isEmail>
      phone string
      address#Address
    }
    
    User {
      name string
      contact#ContactInfo
    }
    Result: User has name, and contact contains email, phone, and address (which itself contains street, city, zipCode).

    Chained References#

    Organization {
      Departments {
        Engineering {
          name string
          headCount number
        }
        Sales {
          name string
          territory string
        }
      }
    }
    
    Team {
      engineering#Organization#Departments#Engineering
      sales#Organization#Departments#Sales
    }

    Optional References#

    Address {
      street string
      city string
    }
    
    User {
      name string
      email string
      homeAddress#Address        // Required
      workAddress#Address?       // Optional
      billingAddress#Address?    // Optional
    }

    Reference Arrays#

    Tag {
      name string<lowercase>
      color string
    }
    
    Article {
      title string
      content string
      tags#Tag[]              // Array of Tag references
      relatedTags#Tag[]?      // Optional array of Tags
    }

    Modifying Referenced Types#

    BaseUser {
      firstName string
      lastName string
      email string<isEmail>
      age number<min:13>
      role string(user|admin|moderator)
      bio? string
    }
    
    // Use as-is
    StandardUser {
      user#BaseUser
    }
    
    // Make some fields optional
    GuestUser {
      user#BaseUser {
        email?    // Email becomes optional for guests
        age?      // Age becomes optional
      }
    }
    
    // Narrow enums
    AdminUser {
      user#BaseUser {
        role string(admin)  // Only admin role allowed
      }
    }
    
    // Combine modifications
    RestrictedUser {
      user#BaseUser {
        age?                         // Make optional
        bio                          // Make required
        role string(user|moderator)  // Narrow enum
      }
    }

    Escaping # in Names#

    Config {
      "api#key" string           // # is literal, not a reference
      "user#id" string           // # is literal
      "field#123" number         // # is literal
      
      apiToken#AuthToken         // This IS a reference to AuthToken
    }

    Key Takeaways#

    1.
    Use # for references - Links fields to other type definitions
    2.
    Same file only - References resolve within the current schema file
    3.
    Forward references work - But define types first for readability
    4.
    Chain references - Use #Type1#Type2 for nested type access
    5.
    No circular references - Type A cannot reference Type B if Type B references Type A
    6.
    No self-references - Types cannot reference themselves
    7.
    Limited modifications - Can only toggle optionality and narrow enums
    8.
    Strict constraints - Cannot modify modifiers, add/remove fields, change types, or add references in modification blocks
    9.
    Quote to escape - Use "field#name" to treat # as literal character
    For common mistakes with references, see Appendix: Common Pitfalls.

    Aliases with :#

    Aliases allow you to accept input with one name but output with another, providing flexibility in API design and data transformation.

    Basic Syntax#

    Use the colon : to create an alias following the pattern inputName:outputName:
    fname:firstName string
    lname:lastName string
    email:emailAddress string
    Semantics:
    Input accepts the name before the colon (fname)
    Output uses the name after the colon (firstName)

    How Aliases Work#

    When processing data:
    User {
      id string
      fname:firstName string
      lname:lastName string
      email:emailAddress string<isEmail>
    }
    Input (what you accept):
    {
      "id": "abc123",
      "fname": "John",
      "lname": "Doe",
      "email": "john@example.com"
    }
    Output (what you produce):
    {
      "id": "abc123",
      "firstName": "John",
      "lastName": "Doe",
      "emailAddress": "john@example.com"
    }

    Alias Positioning#

    According to the declaration ordering rule baseName#reference:alias[length]?, aliases come in position 3, after references:
    // Just alias
    fname:firstName string
    
    // Reference + alias
    contact#Person:primaryContact
    
    // Alias + array
    tags:categories[] string
    
    // Reference + alias + array
    items#Product:productList[]
    
    // All components
    owners#Person:administrators[]?

    One Alias Per Declaration#

    You can only have one alias per declaration. Chaining multiple colons is not allowed:
    // Valid
    fname:firstName string
    
    // Invalid - multiple aliases not allowed
    fname:middleName:firstName string  // ❌ Error
    a:b:c string                       // ❌ Error

    Special Characters in Aliases#

    The # symbol in an alias has no reference meaning - it's treated as a literal character:
    field:output#name string
    // Input: field
    // Output: output#name (# is literal, like "output#name")
    
    data:user#id string
    // Input: data
    // Output: user#id (# is literal)
    Think of the output name as the left-hand side of a JavaScript object property, where special characters are allowed:

    Quoted Aliases#

    Aliases follow the same quoting rules as baseName. Use quotes when the output name contains spaces or special parser characters:
    // Spaces in output name
    fname:"First Name" string
    age:"Age (years)" number
    
    // Special characters in output name
    id:"user-id" string
    email:"user@email" string
    
    // Both input and output quoted
    "in-field":"out-field" string
    "user_name":"User Name" string

    Empty or Missing Parts#

    Both the input name and output name are required. These are invalid:
    field: string        // ❌ Missing output name
    :output string       // ❌ Missing input name
    : string             // ❌ Missing both

    Aliases with References#

    Aliases work seamlessly with references:
    Person {
      name string
      age number
    }
    
    User {
      id string
      contact#Person:primaryContact
      backup#Person:emergencyContact?
    }
    Result:
    The field is referenced as User.contact or User.backup (using the input name)
    But in output, they become primaryContact and emergencyContact

    Aliases with Arrays#

    Combine aliases with arrays:
    Tag {
      name string
      color string
    }
    
    Article {
      items:products[] string
      tags#Tag:categories[]
      notes:comments[10]? string
    }
    Input field names: items, tags, notes
    Output field names: products, categories, comments

    Examples#

    API Field Mapping#

    CreateUserRequest {
      fname:firstName string<trim>
      lname:lastName string<trim>
      email:emailAddress string<isEmail|lowercase>
      pwd:password string<minLength:8>
      dob:dateOfBirth string<isISO>
    }
    Accepts input like:
    {
      "fname": "Jane",
      "lname": "Smith",
      "email": "jane@example.com",
      "pwd": "secretpass",
      "dob": "1990-05-15"
    }
    Outputs:
    {
      "firstName": "Jane",
      "lastName": "Smith",
      "emailAddress": "jane@example.com",
      "password": "secretpass",
      "dateOfBirth": "1990-05-15"
    }

    Database Column Mapping#

    User {
      id string<ulid>
      fname:first_name string
      lname:last_name string
      email:email_address string<isEmail>
      created:created_at string<isISO>
      updated:updated_at string<isISO>
    }
    Input uses short names, output uses database column convention with underscores.

    Legacy System Integration#

    Product {
      id:product_id string
      name:product_name string
      desc:product_description string
      price:unit_price number<min:0>
      qty:quantity_available number<min:0>
    }
    Accept modern short names, output to legacy system's verbose names.

    Complex Aliases with References#

    Address {
      street string
      city string
      zipCode string
    }
    
    Contact {
      name string
      email string
    }
    
    Company {
      name string
      addr#Address:primaryAddress
      billing#Address:billingAddress
      owner#Contact:primaryContact
      tech#Contact:technicalContact?
    }

    Aliases with Special Characters#

    Config {
      key:api#key string
      token:auth#token string
      endpoint:api.endpoint string
      user:user@domain string
    }
    Input: key, token, endpoint, user
    Output: api#key, auth#token, api.endpoint, user@domain

    Aliases with Quoted Names#

    UserProfile {
      fname:"First Name" string
      lname:"Last Name" string
      age:"Age (years)" number
      addr:"Home Address" string
      "phone-number":"Phone Number" string
    }

    Mixed Patterns#

    Organization {
      // Simple alias
      name:organizationName string
      
      // Alias with reference
      owner#Person:primaryOwner
      
      // Alias with array
      tags:categories[] string
      
      // Alias with reference and array
      employees#Person:staff[]
      
      // Alias with reference, array, and optional
      advisors#Person:boardAdvisors[]?
      
      // All components
      contacts#Contact:emergencyContacts[3]?
    }

    Real-World: E-commerce Order#

    Address {
      street string
      city string
      zipCode string
    }
    
    OrderItem {
      productId string
      quantity number
      price number
    }
    
    Order {
      id:orderId string<ulid>
      custId:customerId string<ulid>
      items#OrderItem:orderItems[]
      subtotal:subTotalAmount number<min:0>
      tax:taxAmount number<min:0>
      total:totalAmount number<min:0>
      ship#Address:shippingAddress
      bill#Address:billingAddress
      created:createdAt string<isISO>
      status:orderStatus string(pending|confirmed|shipped|delivered)
    }
    Input uses abbreviated names, output uses full descriptive names.

    Key Takeaways#

    1.
    Input to output mapping - Format: inputName:outputName
    2.
    One alias only - Cannot chain multiple colons (no a:b:c)
    3.
    Position matters - Aliases come after references, before arrays: #reference:alias[]
    4.
    Special chars are literal - # in aliases has no reference meaning
    5.
    Quoting rules apply - Use quotes for spaces and special parser characters
    6.
    Both parts required - Must have both input and output names
    7.
    Works with all components - Combine with references, arrays, and optional markers
    For common mistakes with aliases, see Appendix: Common Pitfalls.

    Arrays with []#

    Arrays allow you to define fields that contain multiple values. SPECML supports both variable-length and fixed-length arrays.

    Basic Array Syntax#

    Use square brackets [] to declare an array:
    tags[] string
    scores[] number
    flags[] boolean
    items[] object
    Position in declaration order: Arrays come in position 4: baseName#reference:alias[length]?
    Arrays must appear after references and aliases, but before the optional marker.

    Variable-Length Arrays#

    Empty brackets [] indicate an array of any length (one or more elements):
    User {
      tags[] string
      scores[] number
      emails[] string<isEmail>
    }
    Validation: The array must contain at least one element. Empty arrays are not valid for variable-length declarations.
    Valid data:
    {
      "tags": ["javascript", "nodejs"],
      "scores": [85, 92, 78],
      "emails": ["user@example.com"]
    }
    Invalid data:
    {
      "tags": [],  // ❌ Empty array not allowed
      "scores": []  // ❌ Empty array not allowed
    }

    Fixed-Length Arrays#

    Specify a number inside brackets [n] to require an exact number of elements:
    User {
      topScores[3] number
      recentTags[5] string
      coordinates[2] number
    }
    Validation: The array must contain exactly the specified number of elements.
    Valid data:
    {
      "topScores": [95, 87, 82],
      "recentTags": ["a", "b", "c", "d", "e"],
      "coordinates": [40.7128, -74.0060]
    }
    Invalid data:
    {
      "topScores": [95, 87],        // ❌ Need exactly 3
      "topScores": [95, 87, 82, 90], // ❌ Need exactly 3
      "coordinates": [40.7128]       // ❌ Need exactly 2
    }

    Array Length Rules#

    Length must be a non-negative integer: [0], [1], [100] are syntactically valid
    Negative numbers are invalid: [-1] is an error
    Decimals are invalid: [5.5] is an error
    [0] means exactly zero elements (effectively an empty array requirement)

    Arrays of Primitive Types#

    Arrays can contain any primitive type:
    Product {
      tags[] string
      prices[] number
      features[] boolean
      metadata[] object
    }

    Arrays Without Type Specification#

    If you don't specify a type, the array can contain mixed types:
    User {
      data[]        // Can be [1, "string", true, {}, []]
      mixed[]       // Can contain any types
    }
    Valid data:
    {
      "data": [1, "hello", true, {"key": "value"}, [1, 2, 3]],
      "mixed": ["text", 42, false]
    }

    Arrays of References#

    Reference other types in arrays:
    Tag {
      name string
      color string
    }
    
    Address {
      street string
      city string
    }
    
    User {
      tags#Tag[]
      addresses#Address[]
      contacts#Contact[3]
    }
    Remember: Arrays come AFTER references: tags#Tag[] not tags[]#Tag

    Arrays with Aliases#

    Combine arrays with aliases for input/output mapping:
    User {
      tags:categories[] string
      items:products[] object
      notes:comments[5] string
    }

    Optional Arrays#

    Make arrays optional by adding ? at the end:
    User {
      tags[]? string          // Optional variable-length array
      scores[10]? number      // Optional fixed-length array
      addresses#Address[]?    // Optional array of references
    }
    When optional:
    The field can be omitted entirely
    If present, it must still follow the array length rules
    Empty arrays are still invalid for variable-length arrays
    Valid data:
    {
      // Field omitted - valid because optional
    }
    
    {
      "tags": ["javascript"]  // Valid - has elements
    }
    Invalid data:
    {
      "tags": []  // ❌ Still invalid - empty array not allowed even when optional
    }

    Arrays in Nested Structures#

    Arrays work at any nesting level:
    Company {
      name string
      departments[] {
        name string
        employees[] {
          id string
          name string
          skills[] string
        }
      }
    }

    Whitespace in Array Syntax#

    Whitespace is allowed inside brackets for readability:
    tags[] string
    tags [ ] string
    tags [5] string
    tags [ 5 ] string
    All variations above are equivalent.

    Examples#

    Simple Arrays#

    BlogPost {
      title string
      content string
      tags[] string
      viewCounts[] number
      isPublished[] boolean
    }

    Fixed-Length Arrays#

    GeoLocation {
      coordinates[2] number    // [latitude, longitude]
      bounds[4] number         // [north, south, east, west]
    }
    
    ColorPalette {
      primaryColors[3] string  // Exactly 3 colors
      rgbValues[3] number      // [red, green, blue]
    }

    Arrays of Objects#

    Order {
      items[] {
        productId string
        quantity number
        price number
      }
      
      shippingSteps[3] {
        location string
        timestamp string<isISO>
        status string
      }
    }

    Arrays with References#

    Contact {
      name string
      email string
      phone string
    }
    
    Product {
      id string
      name string
      price number
    }
    
    Order {
      id string
      customerId string
      items#Product[]
      contacts#Contact[2]
      notes[]? string
    }

    Arrays with Aliases#

    User {
      items:products[] string
      tags:categories[] string
      notes:comments[5] string
      contacts#Person:emergencyContacts[3]
    }
    Input uses: items, tags, notes, contacts
    Output uses: products, categories, comments, emergencyContacts

    Optional Arrays#

    User {
      email string
      tags[]? string              // Optional
      addresses#Address[]?        // Optional
      backupEmails[3]? string     // Optional, but if present must have exactly 3
    }

    Mixed-Type Arrays#

    Config {
      settings[]       // Can be ["string", 42, true, {}]
      mixedData[]      // Any combination of types
      values[]         // No type restriction
    }
    Valid data:
    {
      "settings": ["debug", true, 8080, {"timeout": 3000}],
      "mixedData": [1, "two", false],
      "values": ["a", 1, {"key": "val"}]
    }

    Real-World: E-commerce#

    Image {
      url string<isUrl>
      alt string
    }
    
    Review {
      rating number<min:1|max:5>
      comment string
      author string
    }
    
    Product {
      id string<ulid>
      name string
      description string
      price number<min:0>
      
      images#Image[]
      reviews#Review[]?
      tags[] string
      relatedProductIds[] string
      
      dimensions[3] number       // [length, width, height]
      colors[] string
      sizes[] string(XS|S|M|L|XL|XXL)
    }

    Real-World: Social Media#

    User {
      id string
      username string
    }
    
    HashTag {
      text string
      count number
    }
    
    Comment {
      id string
      text string
      authorId string
      timestamp string<isISO>
    }
    
    Post {
      id string<ulid>
      content string
      authorId string
      
      mentions#User[]?
      hashtags#HashTag[]
      comments#Comment[]?
      
      mediaUrls[]? string<isUrl>
      likes[] string            // Array of user IDs
      
      coordinates[2]? number    // [lat, long] - optional
    }

    Real-World: Data Analytics#

    DataPoint {
      timestamp string<isISO>
      value number
    }
    
    Metric {
      name string
      unit string
      dataPoints#DataPoint[]
      
      summary[5] number      // [min, max, avg, median, stddev]
      percentiles[4] number  // [25th, 50th, 75th, 95th]
    }
    
    Dashboard {
      title string
      metrics#Metric[]
      tags[] string
      refreshInterval number
    }

    Array Limitations#

    No Nested Arrays#

    SPECML does not currently support arrays of arrays:
    // Not supported
    matrix[][] number
    data[][] string
    nested[][] object
    For nested array structures, consider using objects or alternative modeling.

    Minimum Length Requirement#

    Variable-length arrays must contain at least one element. If you need to allow empty arrays, make the field optional and handle the absence of the field:
    // Empty arrays not allowed
    tags[] string
    
    // But you can make the field optional
    tags[]? string
    
    // Then handle these cases:
    // Field omitted entirely ✓
    // Field present with elements ✓
    // Field present but empty ❌

    Key Takeaways#

    1.
    Variable-length arrays - Use [] for one or more elements
    2.
    Fixed-length arrays - Use [n] for exactly n elements
    3.
    Position matters - Arrays come after references and aliases: #ref:alias[]
    4.
    At least one element - Variable-length arrays cannot be empty
    5.
    Mixed types allowed - Arrays without type specification accept any types
    6.
    Optional arrays - Use ? after brackets: tags[]?
    7.
    No nested arrays - Arrays of arrays not currently supported
    8.
    Works everywhere - Arrays can be used at any nesting level
    For common mistakes with arrays, see Appendix: Common Pitfalls.

    Optional Fields with ?#

    By default, all fields in SPECML are required. The optional marker ? allows you to make fields optional, meaning they can be omitted from the data.

    Basic Syntax#

    Add ? at the end of the declaration to mark a field as optional:
    User {
      email string        // Required
      phone? string       // Optional
      bio? string         // Optional
    }
    Position in declaration order: The ? always comes last: baseName#reference:alias[length]?

    Required vs Optional#

    Required fields (default):
    Must be present in the data
    Cannot be omitted
    Validation always applies
    Optional fields (with ?):
    Can be omitted entirely
    Can be undefined
    If present, validation still applies
    User {
      name string              // Must be present
      age number               // Must be present
      bio? string              // Can be omitted
      website? string<isUrl>   // Can be omitted
    }
    Valid data:
    {
      "name": "John Doe",
      "age": 30
      // bio and website omitted - valid because optional
    }
    
    {
      "name": "Jane Smith",
      "age": 25,
      "bio": "Software engineer"
      // website omitted - still valid
    }
    Invalid data:
    {
      "name": "John Doe"
      // age missing - invalid because required
    }

    Optional vs Null#

    Optional means the field can be omitted or undefined. It does NOT automatically mean the field can be null.
    User {
      bio? string
    }
    Valid:
    {
      // bio omitted - valid
    }
    
    {
      "bio": undefined  // Valid
    }
    Null handling:
    {
      "bio": null  // Depends on implementation/modifiers
    }
    Use modifiers to control null behavior:
    isNonNull - Field cannot be null
    isNull - Field must be null
    User {
      bio? string<isNonNull>     // Optional, but if present cannot be null
      deletedAt? string<isNull>  // Optional, and must be null if present
    }

    Validation on Optional Fields#

    When an optional field IS provided, all validation rules still apply:
    User {
      email? string<isEmail|lowercase>
      age? number<min:18|max:120>
      website? string<isUrl>
    }
    Valid:
    {
      // All fields omitted - valid
    }
    
    {
      "email": "user@example.com",  // Valid email format
      "age": 25                      // Within min/max range
    }
    Invalid:
    {
      "email": "invalid-email"  // ❌ Fails validation even though optional
    }
    
    {
      "age": 15  // ❌ Below minimum even though optional
    }

    Optional with Different Types#

    Optional works with all field types:
    User {
      // Optional primitives
      bio? string
      age? number
      isActive? boolean
      
      // Optional objects
      metadata? object
      
      // Optional arrays
      tags[]? string
      scores[10]? number
      
      // Optional references
      address#Address?
      profile#UserProfile?
    }

    Optional References#

    When a field referencing another type is optional, the entire reference is optional:
    Address {
      street string    // Required in Address
      city string      // Required in Address
      zipCode string   // Required in Address
    }
    
    User {
      name string
      homeAddress#Address?    // Address is optional
      workAddress#Address     // Address is required
    }
    Behavior:
    homeAddress can be omitted entirely
    If homeAddress is provided, it must have street, city, and zipCode (all required within Address)
    workAddress must be provided and must have all required Address fields
    Valid data:
    {
      "name": "John Doe",
      "workAddress": {
        "street": "123 Main St",
        "city": "New York",
        "zipCode": "10001"
      }
      // homeAddress omitted - valid because optional
    }
    Invalid data:
    {
      "name": "John Doe",
      "homeAddress": {
        "street": "123 Main St"
        // Missing city and zipCode - invalid even though homeAddress is optional
      },
      "workAddress": {
        "street": "456 Oak Ave",
        "city": "Boston",
        "zipCode": "02101"
      }
    }

    Optional Arrays#

    Optional arrays can be omitted, but if present, they must follow array rules:
    User {
      tags[]? string           // Optional array
      scores[10]? number       // Optional fixed-length array
      addresses#Address[]?     // Optional array of references
    }
    Valid data:
    {
      // All arrays omitted - valid
    }
    
    {
      "tags": ["javascript", "nodejs"]  // Present with elements - valid
    }
    Invalid data:
    {
      "tags": []  // ❌ Empty array still invalid even when optional
    }
    
    {
      "scores": [85, 92]  // ❌ Must have exactly 10 elements if present
    }

    Optional with Aliases#

    Combine optional with aliases:
    User {
      fname:firstName string
      lname:lastName string
      mname:middleName? string
      nick:nickname? string
    }
    Input accepts: fname, lname, mname, nick
    Output uses: firstName, lastName, middleName, nickname
    Optional: mname and nick can be omitted

    Optional Everywhere#

    The ? marker works at any nesting level:
    Company {
      name string
      address? {
        street string
        city string
        zip? string       // zip is optional within address
      }
      employees[] {
        name string
        title? string     // title is optional within each employee
        department? {
          name string
          manager? string // manager is optional within department
        }
      }
    }

    Examples#

    User Profile#

    User {
      id string<ulid>
      email string<isEmail>
      username string<minLength:3>
      
      // Optional profile fields
      firstName? string
      lastName? string
      bio? string<maxLength:500>
      avatarUrl? string<isUrl>
      website? string<isUrl>
      phone? string
      
      // Required timestamps
      createdAt string<isISO>
      updatedAt string<isISO>
    }

    Product Catalog#

    Product {
      id string<ulid>
      name string
      description string
      price number<min:0>
      
      // Optional fields
      compareAtPrice? number<min:0>
      costPrice? number<min:0>
      sku? string
      barcode? string
      
      // Optional arrays
      tags[]? string
      images[]? string<isUrl>
      
      // Required status
      isActive boolean
    }

    API Request/Response#

    CreateUserRequest {
      email string<isEmail>
      password string<minLength:8>
      username string<minLength:3>
      
      // Optional during creation
      firstName? string
      lastName? string
      phoneNumber? string
    }
    
    CreateUserResponse {
      id string<ulid>
      email string
      username string
      
      // Optional in response
      firstName? string
      lastName? string
      phoneNumber? string
      
      createdAt string<isISO>
    }

    Nested Optional Fields#

    ContactInfo {
      email string<isEmail>
      phone? string
      address? {
        street string
        city string
        state string
        zipCode? string
      }
    }
    
    User {
      name string
      contactInfo? #ContactInfo  // Entire contact info is optional
      emergencyContact? {
        name string
        relationship string
        phone string
      }
    }

    Optional References#

    Address {
      street string
      city string
      state string
      zipCode string
    }
    
    PaymentMethod {
      type string
      last4 string
    }
    
    Order {
      id string<ulid>
      customerId string
      total number
      
      // Optional references
      shippingAddress#Address?
      billingAddress#Address?
      paymentMethod#PaymentMethod?
      
      // Optional primitive
      notes? string
      giftMessage? string
      
      // Required fields
      status string
      createdAt string<isISO>
    }

    Complex Optional Patterns#

    Timestamps {
      createdAt string<isISO>
      updatedAt string<isISO>
    }
    
    Author {
      id string
      name string
      email string
    }
    
    Category {
      id string
      name string
    }
    
    Article {
      id string<ulid>
      title string<minLength:1|maxLength:200>
      content string
      
      // Required reference
      author#Author
      timestamps#Timestamps
      
      // Optional reference
      primaryCategory#Category?
      
      // Optional arrays
      tags[]? string
      categories#Category[]?
      relatedArticles[]? string
      
      // Optional fields with validation
      excerpt? string<maxLength:300>
      featuredImageUrl? string<isUrl>
      publishedAt? string<isISO>
      
      // Required status
      status string(draft|published|archived)
    }

    Real-World: E-commerce Order#

    Address {
      street string
      city string
      state string
      zipCode string
      country string
    }
    
    Customer {
      id string
      email string
      firstName string
      lastName string
    }
    
    OrderItem {
      productId string
      productName string
      quantity number<min:1>
      unitPrice number<min:0>
      subtotal number<min:0>
    }
    
    Order {
      id string<ulid>
      orderNumber string<unique>
      
      // Required references
      customer#Customer
      items#OrderItem[]
      shippingAddress#Address
      
      // Optional reference
      billingAddress#Address?  // Use shipping if not provided
      
      // Required amounts
      subtotal number<min:0>
      tax number<min:0>
      shipping number<min:0>
      total number<min:0>
      
      // Optional fields
      discountCode? string
      discountAmount? number<min:0>
      giftWrap? boolean
      giftMessage? string<maxLength:500>
      specialInstructions? string
      
      // Optional tracking
      trackingNumber? string
      carrier? string
      estimatedDelivery? string<isISO>
      
      // Required status
      status string(pending|processing|shipped|delivered|cancelled)
      createdAt string<isISO>
    }

    When to Use Optional Fields#

    Use Optional For:#

    1.
    User-provided data that may not always be available
    User {
      email string
      phoneNumber? string  // Not everyone provides phone
    }
    2.
    Fields that have sensible defaults
    Config {
      host string
      port? number  // Default to 3000 if not provided
    }
    3.
    Progressive data collection
    Profile {
      username string
      bio? string          // Can be added later
      avatarUrl? string    // Can be added later
    }
    4.
    Conditional features
    Product {
      price number
      salePrice? number    // Only if on sale
    }

    Keep Required For:#

    1.
    Critical business data
    User {
      id string
      email string  // Always needed for user
    }
    2.
    Data integrity requirements
    Order {
      customerId string
      total number  // Must always be calculated
    }
    3.
    Fields needed for core functionality
    Article {
      title string
      content string  // Can't publish without content
    }

    Key Takeaways#

    1.
    All fields are required by default - Use ? to make optional
    2.
    Position matters - ? always comes last: baseName#reference:alias[length]?
    3.
    Can be omitted - Optional means the field can be absent or undefined
    4.
    Validation still applies - If present, optional fields must pass validation
    5.
    Not the same as null - Optional ≠ nullable (use modifiers for null handling)
    6.
    Works with everything - Optional works with primitives, objects, arrays, and references
    7.
    Nested optionality - Can mark fields optional at any level of nesting
    8.
    Reference behavior - If optional reference is present, its required fields are still required
    For common mistakes with optional fields, see Appendix: Common Pitfalls.

    Copy Operator > and ...#

    The copy operator allows you to copy all fields from another object into the current context, enabling powerful composition and inheritance patterns.

    Basic Syntax#

    Use > or ... to copy fields from another object:
    BaseUser {
      id string
      email string
      createdAt string
    }
    
    User {
      >BaseUser          // Copies id, email, createdAt
      name string        // Adds name
    }
    Result: User contains id, email, createdAt, and name.
    Both forms are equivalent and interchangeable:
    User {
      >BaseUser     // Shorthand
      name string
    }
    
    User {
      ...BaseUser   // Longhand
      name string
    }
    You can mix both forms in the same file:
    User {
      >BaseUser
      ...Timestamps
      name string
    }

    How Copy Works#

    Copy operations bring all fields from the source object into the current scope:
    Timestamps {
      createdAt string<isISO>
      updatedAt string<isISO>
    }
    
    AuditFields {
      createdBy string<ulid>
      updatedBy string<ulid>
    }
    
    Article {
      >Timestamps     // Adds createdAt, updatedAt
      >AuditFields    // Adds createdBy, updatedBy
      title string
      content string
    }
    Result: Article has 6 fields: createdAt, updatedAt, createdBy, updatedBy, title, content.

    Root-Level Copy#

    Copy operations work at the root level too:
    GlobalConfig {
      apiVersion string
      timeout number
    }
    
    >GlobalConfig    // Copies fields into root scope
    
    User {
      name string
      email string
    }

    Copy with References#

    Copy can use references to specify which object to copy from:
    Organization {
      Settings {
        theme string
        language string
      }
    }
    
    App {
      >config#Organization#Settings    // Copy from nested type
      appName string
    }
    Chained references work:
    >someObject#Type1#Type2    // Copy from Type2 within Type1
    Direct references not allowed:
    >#SomeType    // ❌ Error: Cannot copy from reference without base object

    Copy with String Literals#

    Use quotes if the object name contains special characters:
    >"some Object"
    >'another-object'
    >"config#settings"

    Field Redeclaration and Override#

    When a copied field conflicts with a local declaration, the local declaration wins:
    BaseUser {
      name string
      age number
      email string
    }
    
    User {
      >BaseUser
      name number    // Overrides name from BaseUser (was string, now number)
      bio string     // Adds new field
    }
    Result: User has name as number (overridden), age as number (from BaseUser), email as string (from BaseUser), and bio as string (new).
    Note: This behavior can be controlled via configuration (see Configuration section).

    Multiple Copy Operations#

    You can copy from multiple sources in the same block:
    Timestamps {
      createdAt string<isISO>
      updatedAt string<isISO>
    }
    
    AuditFields {
      createdBy string<ulid>
      updatedBy string<ulid>
    }
    
    SoftDelete {
      deletedAt? string<isISO>
      isDeleted boolean
    }
    
    Article {
      >Timestamps
      >AuditFields
      >SoftDelete
      title string
      content string
    }
    Order matters: If multiple copied objects have the same field, later copies override earlier ones:
    Object1 {
      name string
    }
    
    Object2 {
      name number
    }
    
    User {
      >Object1    // name is string
      >Object2    // name becomes number (later copy wins)
    }

    Transitive Copy#

    Copy operations are transitive - if ObjectA copies from ObjectB, and you copy from ObjectA, you get both ObjectA's fields AND ObjectB's fields:
    BaseObject {
      id string
      createdAt string
    }
    
    ExtendedObject {
      >BaseObject    // Has id, createdAt
      name string
    }
    
    FinalObject {
      >ExtendedObject    // Gets id, createdAt (from BaseObject) AND name (from ExtendedObject)
      description string
    }
    Result: FinalObject has id, createdAt, name, and description.

    Selective Copy with Modifiers#

    Control which fields are copied using select or exclude modifiers.

    Select Specific Fields#

    Copy only specified fields using <select:fieldNames>:
    FullUser {
      id string
      email string
      passwordHash string
      internalNotes string
      firstName string
      lastName string
    }
    
    PublicUser {
      >FullUser<select:id,email,firstName,lastName>
    }
    Result: PublicUser has only id, email, firstName, and lastName.

    Exclude Specific Fields#

    Copy all fields except specified ones using <exclude:fieldNames>:
    FullUser {
      id string
      email string
      passwordHash string
      internalNotes string
      firstName string
      lastName string
    }
    
    SafeUser {
      >FullUser<exclude:passwordHash,internalNotes>
    }
    Result: SafeUser has id, email, firstName, and lastName.

    Field Name Rules in Select/Exclude#

    Field names in select and exclude modifiers can contain:
    Letters and numbers
    Underscores _
    Periods .
    Hashes #
    For other special characters (commas, hyphens), use quotes:
    >someObject<select:field_name>           // Underscore OK
    >someObject<select:field.name>           // Period OK
    >someObject<select:field#ref>            // Hash OK
    >someObject<select:"field-name">         // Hyphen requires quotes
    >someObject<select:"field,name">         // Comma requires quotes
    >someObject<select:field1,"field,2",field3>  // Mixed

    Non-Existent Fields#

    Selecting or excluding fields that don't exist in the source object is valid - they're simply ignored:
    User {
      name string
      email string
    }
    
    Profile {
      >User<select:name,email,bio>    // bio doesn't exist - ignored
      >User<exclude:phone>             // phone doesn't exist - ignored
    }

    Select/Exclude with References#

    Combine selective copy with references:
    Organization {
      Settings {
        theme string
        language string
        apiKey string
        secretToken string
      }
    }
    
    PublicConfig {
      >config#Organization#Settings<exclude:apiKey,secretToken>
    }

    Negation in Copy Modifiers#

    Use ! to negate modifiers:
    >someObject<!exclude:age>    // !exclude = select
    >someObject<!select:age>     // !select = exclude
    Equivalent pairs:
    >User<select:name,email>
    >User<!exclude:name,email>   // Same as above
    
    >User<exclude:password>
    >User<!select:password>      // Same as above

    Copy Restrictions#

    Cannot Combine Select and Exclude#

    You cannot use both select and exclude in the same copy operation:
    // Invalid
    >someObject<select:a|exclude:b>    // ❌ Error
    >someObject<select:a,b|exclude:c>  // ❌ Error
    Use one or the other:
    // Valid
    >someObject<select:a,b,c>
    >someObject<exclude:x,y,z>

    Copy Cannot Have Aliases#

    Copy operations cannot have aliases:
    >someObject:alias    // ❌ Error: Copy cannot have alias

    Copy Cannot Be Array or Optional#

    Copy operations cannot use array or optional markers:
    >someObject[]    // ❌ Error: Copy cannot be array
    >someObject?     // ❌ Error: Copy cannot be optional

    Copy Cannot Have Expressions#

    Copy is a declaration-only operation. It cannot have an expression:
    >someObject string      // ❌ Error: Copy cannot have expression
    >someObject {           // ❌ Error: Copy cannot have block
      field string
    }

    Copy Cannot Have Grouped Modifiers#

    Only select and exclude modifiers are allowed, and they cannot be grouped:
    >someObject<(select:age)>              // ❌ Error: Cannot group
    >someObject<select:a|required>         // ❌ Error: Only select/exclude allowed
    >someObject<select:a|minLength:5>      // ❌ Error: Only select/exclude allowed

    No Self-Reference#

    An object cannot copy from itself:
    User {
      >User    // ❌ Error: Self-reference not allowed
    }

    No Circular Dependencies#

    Objects cannot have circular copy dependencies:
    ObjectA {
      >ObjectB
    }
    
    ObjectB {
      >ObjectA    // ❌ Error: Circular dependency
    }

    Empty Object Copy#

    Copying from an empty object is valid but copies nothing:
    EmptyObject {
    }
    
    User {
      >EmptyObject    // Valid but adds no fields
      name string
    }

    Examples#

    Basic Composition#

    Timestamps {
      createdAt string<isISO>
      updatedAt string<isISO>
    }
    
    User {
      >Timestamps
      id string<ulid>
      email string<isEmail>
      name string
    }
    
    Article {
      >Timestamps
      id string<ulid>
      title string
      content string
    }

    Multiple Copy Operations#

    Identifiable {
      id string<ulid>
    }
    
    Timestamps {
      createdAt string<isISO>
      updatedAt string<isISO>
    }
    
    Auditable {
      createdBy string<ulid>
      updatedBy string<ulid>
    }
    
    SoftDeletable {
      deletedAt? string<isISO>
      isDeleted boolean
    }
    
    Post {
      >Identifiable
      >Timestamps
      >Auditable
      >SoftDeletable
      title string
      content string
      authorId string<ulid>
    }

    Selective Copy for Data Privacy#

    InternalUser {
      id string<ulid>
      email string<isEmail>
      passwordHash string
      saltValue string
      failedLoginAttempts number
      lastLoginIp string
      internalNotes string
      firstName string
      lastName string
      role string
      isActive boolean
    }
    
    // For public API responses
    PublicUser {
      >InternalUser<select:id,email,firstName,lastName,isActive>
    }
    
    // For admin dashboard
    AdminUser {
      >InternalUser<exclude:passwordHash,saltValue>
    }
    
    // For authentication context
    AuthUser {
      >InternalUser<select:id,email,role,isActive>
    }

    Layered Configuration#

    DefaultConfig {
      host string
      port number
      timeout number
      retries number
      logLevel string
    }
    
    ProductionConfig {
      >DefaultConfig
      host string              // Override host
      enableMetrics boolean    // Add new field
    }
    
    DevelopmentConfig {
      >DefaultConfig
      logLevel string          // Override to different level
      debugMode boolean        // Add new field
    }

    Shared API Fields#

    ApiMetadata {
      apiVersion string
      requestId string<ulid>
      timestamp string<isISO>
    }
    
    SuccessResponse {
      >ApiMetadata
      success boolean
      data object
    }
    
    ErrorResponse {
      >ApiMetadata
      success boolean
      error string
      message string
      details? object
    }

    Transitive Composition#

    BaseEntity {
      id string<ulid>
      createdAt string<isISO>
    }
    
    NamedEntity {
      >BaseEntity
      name string
    }
    
    TaggedEntity {
      >NamedEntity    // Gets id, createdAt, name
      tags[] string
    }
    
    Product {
      >TaggedEntity    // Gets id, createdAt, name, tags
      price number
      description string
    }

    Real-World: E-commerce#

    Identifiable {
      id string<ulid>
    }
    
    Timestamps {
      createdAt string<isISO>
      updatedAt string<isISO>
    }
    
    Auditable {
      createdBy string<ulid>
      updatedBy string<ulid>
    }
    
    PriceInfo {
      price number<min:0>
      currency string(USD|EUR|GBP)
      taxRate? number<min:0|max:1>
    }
    
    Product {
      >Identifiable
      >Timestamps
      >Auditable
      >PriceInfo
      
      name string
      description string
      sku string<unique>
      inStock boolean
      stockQuantity number<min:0>
    }
    
    Order {
      >Identifiable
      >Timestamps
      >Auditable
      
      orderNumber string<unique>
      customerId string<ulid>
      total number<min:0>
      status string(pending|confirmed|shipped|delivered)
    }

    Real-World: Multi-tenant SaaS#

    TenantAware {
      tenantId string<ulid>
    }
    
    SoftDelete {
      deletedAt? string<isISO>
      deletedBy? string<ulid>
    }
    
    Timestamps {
      createdAt string<isISO>
      updatedAt string<isISO>
    }
    
    User {
      >TenantAware
      >Timestamps
      >SoftDelete
      
      id string<ulid>
      email string<isEmail>
      name string
      role string
    }
    
    Document {
      >TenantAware
      >Timestamps
      >SoftDelete
      
      id string<ulid>
      title string
      content string
      ownerId string<ulid>
    }

    Copy with Overrides#

    BaseProduct {
      name string
      price number
      description string
      isActive boolean
    }
    
    DigitalProduct {
      >BaseProduct
      price number<min:0|max:1000>    // Override with stricter constraint
      downloadUrl string<isUrl>       // Add new field
    }
    
    PhysicalProduct {
      >BaseProduct
      weight number<min:0>            // Add new field
      dimensions object               // Add new field
      shippingClass string            // Add new field
    }

    Configuration: Copy Override Behavior#

    The copyOverrideBehaviour configuration option in .SPECML.config.json controls how conflicts are handled:
    {
      "copyOverrideBehaviour": "override"
    }
    Values:
    "error" (default) - Throw error when copied field conflicts with local declaration
    "override" - Local declarations override copied fields

    With "error" mode:#

    BaseObject {
      name string
    }
    
    User {
      >BaseObject
      name number    // ❌ Error: field already declared
    }

    With "override" mode:#

    BaseObject {
      name string
    }
    
    User {
      >BaseObject
      name number    // ✓ Valid: local declaration wins, name becomes number
    }

    Key Takeaways#

    1.
    Two forms - > and ... are completely interchangeable
    2.
    Copies all fields - Brings all fields from source into current scope
    3.
    Multiple copies - Can copy from multiple objects in sequence
    4.
    Local wins - Local declarations override copied fields (configurable)
    5.
    Transitive - Copying includes transitively copied fields
    6.
    Selective copy - Use <select:> or <exclude:> to filter fields
    7.
    Cannot mix - Cannot use both select and exclude in same operation
    8.
    Copy-only - Cannot combine with aliases, arrays, optional, or expressions
    9.
    No self-reference - Objects cannot copy from themselves
    10.
    No circular dependencies - Copy chains must be acyclic
    For common mistakes with copy operations, see Appendix: Common Pitfalls.

    Imports#

    Imports allow you to split schemas across multiple files and compose them together, enabling modular schema organization and reusability.

    What Are Imports?#

    Imports bring the contents of another SPECML file into your current file. Think of imports like the copy operator >, but for entire files instead of individual types.
    // users.spec
    User {
      name string
      email string
    }
    
    // main.spec
    import users.spec
    
    // Now User is available here
    Profile {
      user#User
    }
    The imported file's contents are inserted at the import location, as if you had written them directly in your file.

    Basic Import Syntax#

    Use import followed by the file path:
    import data.spec
    import users.spec
    import ../shared/common.spec
    Rules:
    One import per line
    No semicolon at the end
    Quotes optional (unless path has spaces or special characters)
    Imports can appear anywhere in the file

    Import Path Resolution#

    Current Directory#

    Import files from the same directory:
    import users.spec
    import products.spec
    Resolves to:
    ./users.spec
    ./products.spec
    Relative to the file doing the importing.

    Parent Directory#

    Navigate up the directory tree with ../:
    import ../common.spec
    import ../../shared/base.spec
    Resolves to:
    One directory up: ../common.spec
    Two directories up: ../../shared/base.spec

    Base Directory with @#

    Use @/ to reference the base directory:
    import @/schemas/users.spec
    import @/shared/common.spec
    Base directory resolution:
    1.
    If baseDirectory is configured in .SPECML.config.json, use that path
    2.
    Otherwise, defaults to project root directory
    Configuration:
    {
      "baseDirectory": "/path/to/my/specs"
    }

    Import with Quotes#

    Use quotes when paths contain spaces or special characters:
    import "user data.spec"
    import "schemas/my-file.spec"
    import "../shared files/common.spec"
    Quotes follow the same rules as quoted strings elsewhere in SPECML.

    How Imports Work#

    Imports are processed like inline inclusion. The imported file's contents are inserted at the import location.

    Example#

    users.spec:
    User {
      name string
      email string
    }
    products.spec:
    Product {
      title string
      price number
    }
    main.spec:
    import users.spec
    import products.spec
    
    Order {
      customer#User
      items#Product[]
    }
    Processed as if main.spec contained:
    User {
      name string
      email string
    }
    
    Product {
      title string
      price number
    }
    
    Order {
      customer#User
      items#Product[]
    }

    Import Order Matters#

    Imports are processed in the order they appear:
    import file2.spec
    import file1.spec
    
    // file2.spec content comes first
    // file1.spec content comes second
    Why order matters:
    // base.spec
    Timestamps {
      createdAt string
      updatedAt string
    }
    
    // main.spec
    import base.spec
    
    User {
      >Timestamps    // Works because Timestamps is already imported
      name string
    }
    If you try to use a type before importing it, you'll get a reference error.

    Imports Can Appear Anywhere#

    Unlike some languages, imports don't need to be at the top of the file:
    User {
      name string
      email string
    }
    
    import products.spec    // Import in the middle
    
    Order {
      customer#User
      items#Product[]
    }
    
    import payments.spec    // Import at the end
    This is valid because imports work like inline inclusion at the point they're declared.

    Circular Import Handling#

    When File A imports File B, and File B imports File A, SPECML handles this gracefully.

    Default Behavior#

    file1.spec:
    import file2.spec
    
    Type1 {
      field string
    }
    file2.spec:
    import file1.spec
    
    Type2 {
      field string
    }
    Resolution:
    1.
    file1.spec starts importing file2.spec
    2.
    While processing file2.spec, it encounters import file1.spec
    3.
    SPECML detects the circular reference and ignores the second import
    4.
    Processing continues normally
    Result: Both Type1 and Type2 are available without infinite loops.

    Configuration#

    Control circular import behavior in .SPECML.config.json:
    {
      "circularImportBehavior": "ignore"
    }
    Values:
    "ignore" (default) - Silently ignore circular imports
    "warn" - Log a warning but continue
    "error" - Throw an error on circular imports

    Common Patterns#

    Shared Base Types#

    shared/base.spec:
    Timestamps {
      createdAt string<isISO>
      updatedAt string<isISO>
    }
    
    SoftDelete {
      deletedAt? string<isISO>
      isDeleted boolean
    }
    
    Address {
      street string
      city string
      state string
      zipCode string
      country string<length:2|uppercase>
    }
    users.spec:
    import shared/base.spec
    
    User {
      >Timestamps
      >SoftDelete
      id string<ulid>
      name string
      homeAddress#Address
    }
    products.spec:
    import shared/base.spec
    
    Product {
      >Timestamps
      id string<ulid>
      title string
      price number
    }

    Domain Organization#

    Organize by domain or feature:
    project/
    ├── schemas/
    │   ├── users/
    │   │   ├── user.spec
    │   │   ├── profile.spec
    │   │   └── preferences.spec
    │   ├── orders/
    │   │   ├── order.spec
    │   │   ├── order-item.spec
    │   │   └── payment.spec
    │   ├── shared/
    │   │   ├── base.spec
    │   │   └── common.spec
    │   └── main.spec
    main.spec:
    import shared/base.spec
    import shared/common.spec
    import users/user.spec
    import users/profile.spec
    import orders/order.spec
    import orders/payment.spec

    Base Directory Pattern#

    Set base directory in config:
    {
      "baseDirectory": "./schemas"
    }
    Then use @/ for all imports:
    import @/shared/base.spec
    import @/users/user.spec
    import @/orders/order.spec
    Benefits:
    Consistent import paths
    Easy to move files
    No relative path confusion

    ASPECML with Imports#

    Imports work seamlessly with API specifications:
    shared/responses.spec:
    ErrorResponse {
      error string
      message string
      timestamp string<isISO>
    }
    
    ValidationErrorResponse {
      error string
      message string
      details[] {
        field string
        message string
      }
    }
    shared/headers.spec:
    AuthHeaders {
      Authorization string
      X-API-Key string
    }
    
    StandardHeaders {
      Content-Type string(application/json)
      X-Request-ID string<ulid>
    }
    users-api.spec:
    import shared/responses.spec
    import shared/headers.spec
    
    CreateUserEndpoint {
      method POST
      path /api/users
      
      headers {
        >StandardHeaders
        >AuthHeaders
      }
      
      body {
        email string<isEmail>
        password string<minLength:8>
      }
      
      response {
        success {
          status 201
          body {
            id string
            email string
          }
        }
        
        validation_error {
          status 422
          body#ValidationErrorResponse
        }
        
        server_error {
          status 500
          body#ErrorResponse
        }
      }
    }

    Real-World Example: Fintech Platform#

    Structure:
    fintech-api/
    ├── schemas/
    │   ├── base/
    │   │   ├── timestamps.spec
    │   │   ├── audit.spec
    │   │   └── money.spec
    │   ├── users/
    │   │   ├── customer.spec
    │   │   └── kyc.spec
    │   ├── accounts/
    │   │   ├── account.spec
    │   │   └── transaction.spec
    │   ├── api/
    │   │   ├── headers.spec
    │   │   ├── responses.spec
    │   │   └── endpoints.spec
    │   └── main.spec
    base/timestamps.spec:
    Timestamps {
      createdAt string<isISO>
      updatedAt string<isISO>
    }
    base/audit.spec:
    AuditMetadata {
      createdBy string<ulid>
      updatedBy string<ulid>
    }
    base/money.spec:
    Money {
      amount number
      currency string<uppercase|length:3>(USD|EUR|GBP)
    }
    users/customer.spec:
    import ../base/timestamps.spec
    import ../base/audit.spec
    
    Customer {
      >Timestamps
      >AuditMetadata
      id string<ulid>
      email string<isEmail>
      firstName string
      lastName string
      status string(active|suspended|closed)
    }
    accounts/account.spec:
    import ../base/timestamps.spec
    import ../base/audit.spec
    import ../base/money.spec
    
    Account {
      >Timestamps
      >AuditMetadata
      id string<ulid>
      accountNumber string<unique>
      customerId string<ulid>
      balance#Money
      status string(active|frozen|closed)
    }
    accounts/transaction.spec:
    import ../base/timestamps.spec
    import ../base/money.spec
    
    Transaction {
      >Timestamps
      id string<ulid>
      accountId string<ulid>
      amount#Money
      type string(debit|credit)
      description string
      balanceAfter#Money
    }
    api/headers.spec:
    AuthHeaders {
      Authorization string
      X-API-Key string
    }
    
    StandardHeaders {
      Content-Type string(application/json)
      X-Request-ID string<ulid>
    }
    api/responses.spec:
    import ../base/timestamps.spec
    
    ApiError {
      error string
      message string
      timestamp string<isISO>
      requestId? string
    }
    
    ValidationError {
      >ApiError
      details[] {
        field string
        message string
        code string
      }
    }
    api/endpoints.spec:
    import headers.spec
    import responses.spec
    import ../users/customer.spec
    import ../accounts/account.spec
    import ../accounts/transaction.spec
    
    CreateAccountEndpoint {
      method POST
      path /api/accounts
      
      headers {
        >AuthHeaders
        >StandardHeaders
      }
      
      body {
        customerId string<ulid>
        accountType string(checking|savings)
        initialDeposit? number<min:0>
        currency string(USD|EUR|GBP)
      }
      
      response {
        success {
          status 201
          body#Account
        }
        
        validation_error {
          status 422
          body#ValidationError
        }
        
        unauthorized {
          status 401
          body#ApiError
        }
      }
    }
    
    GetTransactionHistoryEndpoint {
      method GET
      path /api/accounts/:accountId/transactions
      
      headers {
        >AuthHeaders
      }
      
      params {
        accountId string<ulid>
      }
      
      query {
        startDate? string<isISO>
        endDate? string<isISO>
        type? string(debit|credit)
        page? number<min:1>
        limit? number<min:1|max:100>
      }
      
      response {
        success {
          status 200
          body {
            transactions#Transaction[]
            pagination {
              page number
              limit number
              total number
              hasMore boolean
            }
          }
        }
        
        not_found {
          status 404
          body#ApiError
        }
      }
    }
    main.spec:
    import api/endpoints.spec
    
    // All types and endpoints from imported files are now available

    Import Best Practices#

    1. Organize by Domain#

    Group related schemas together:
    schemas/
    ├── users/
    ├── products/
    ├── orders/
    └── shared/

    2. Use Base Directory#

    Configure baseDirectory and use @/ imports for consistency:
    {
      "baseDirectory": "./schemas"
    }
    import @/shared/base.spec
    import @/users/user.spec

    3. Import Shared Types First#

    Import base types before domain types:
    import shared/base.spec        // Base types first
    import shared/common.spec
    import users/user.spec         // Domain types after
    import products/product.spec

    4. One Concern Per File#

    Keep files focused:
    // ✅ Good - single concern
    // timestamps.spec
    Timestamps { ... }
    
    // ✅ Good - single concern  
    // user.spec
    User { ... }
    
    // ❌ Avoid - mixed concerns
    // everything.spec
    Timestamps { ... }
    User { ... }
    Product { ... }
    Order { ... }

    5. Document Import Dependencies#

    Use comments to clarify dependencies:
    // Requires: shared/base.spec for Timestamps
    // Requires: shared/money.spec for Money type
    import shared/base.spec
    import shared/money.spec
    
    Account {
      >Timestamps
      balance#Money
    }

    Imports vs Copy Operator#

    Both imports and the copy operator > compose schemas, but they work at different levels:
    FeatureImportCopy Operator >
    ScopeFile-levelType-level
    Usageimport file.spec>TypeName
    Brings inEntire file contentsSpecific type's fields
    LocationAnywhere in fileInside type definitions
    Circular handlingConfigurableNot allowed
    Use imports for:
    Organizing schemas across files
    Sharing types between projects
    Domain separation
    Use copy operator for:
    Composing types within a file
    Inheriting common fields
    Selective field inclusion
    Together:
    // shared.spec
    Timestamps {
      createdAt string<isISO>
      updatedAt string<isISO>
    }
    
    // users.spec
    import shared.spec
    
    User {
      >Timestamps    // Copy operator
      name string
    }

    Configuration Reference#

    .SPECML.config.json#

    {
      "baseDirectory": "./schemas",
      "circularImportBehavior": "ignore"
    }
    Options:
    baseDirectory (string)
    Path used when imports start with @/
    Relative to project root
    Default: project root directory
    circularImportBehavior (string)
    How to handle circular imports
    Values: "ignore" | "warn" | "error"
    Default: "ignore"

    Key Takeaways#

    1.
    Imports bring file contents - Like inline inclusion
    2.
    One import per line - No semicolons
    3.
    Quotes optional - Unless path has spaces/special chars
    4.
    Three path types - Relative (./), parent (../), base (@/)
    5.
    Order matters - Types must be imported before use
    6.
    Imports work anywhere - Not just at file top
    7.
    Circular imports handled - Configurable behavior
    8.
    Base directory - Configure with baseDirectory for consistency
    9.
    Works with ASPECML - Import API components across files
    10.
    Complements copy operator - File-level vs type-level composition
    Imports enable scalable, maintainable schema organization as your project grows.

    Expressions#

    Expressions define the characteristics of a field. They appear after the declaration, separated by whitespace.
    declaration expression
    The interpretation of expressions depends on the adapter processing your schema. SPECML provides the syntax; adapters provide the semantics.
    Examples:
    A validation adapter might interpret age number as "age must be a number"
    An API adapter might interpret http.method POST as "use POST method"
    A database adapter might interpret id string<unique> as "create unique index"
    This section focuses on expression syntax, not semantic meaning.

    Expression Types#

    Block Expressions#

    An opening brace { indicates the declaration contains nested fields:
    Person {
      name string
      age number
    }
    
    Address {
      street string
      city string
      zipCode string
    }
    The { is both the end of the declaration line and the start of a nested block.

    Simple Expressions#

    Simple expressions are identifiers or keywords:
    age number
    name string
    isActive boolean
    metadata object
    items array
    What these mean depends on your adapter:
    Validation: field types
    API: endpoint configurations
    Database: column types

    Empty Expressions#

    A declaration without an expression is valid:
    data           // No expression
    config         // No expression
    metadata       // No expression
    Interpretation: The field can accept any type of value. The adapter determines how to handle fields without expressions.

    Modifiers with <>#

    Modifiers add constraints, transformations, or metadata to expressions. Enclose modifiers in angle brackets <> and separate multiple modifiers with |:
    age number<min:18|max:120>
    email string<isEmail|lowercase>
    code string<uppercase|length:6>
    price number<min:0>

    Modifier Syntax#

    Basic modifier:
    field expression<modifierName>
    Modifier with argument:
    field expression<modifierName:value>
    Multiple modifiers:
    field expression<modifier1|modifier2|modifier3>
    field expression<modifier1:value1|modifier2:value2>

    Modifier Arguments#

    Arguments follow the modifier name after a colon:
    name string<minLength:2|maxLength:100>
    price number<min:0|max:1000000>
    pattern string<matches:"^[A-Z]{3}$">
    Special characters in arguments:
    When arguments contain | or :, enclose them in quotes:
    field string<pattern:"^[a-z]{2,5}$">           // Regex with special chars
    field string<custom:"value|with|pipes">        // Pipes in value
    url string<startsWith:"http://"|endsWith:.com>
    config string<value:"key:value">               // Colon in value

    Common Modifier Patterns#

    While specific modifiers depend on your adapter, common patterns include:
    String modifiers:
    username string<minLength:3|maxLength:20>
    email string<isEmail|lowercase>
    code string<uppercase|trim>
    slug string<lowercase|trim>
    Number modifiers:
    age number<min:0|max:150>
    price number<min:0>
    quantity number<min:1>
    percentage number<min:0|max:100>
    Format validation:
    email string<isEmail>
    url string<isUrl>
    id string<isUUID>
    date string<isISO>
    Transformations:
    name string<trim|capitalize>
    slug string<lowercase|trim>
    code string<uppercase>

    Empty Modifiers#

    Empty modifier brackets are valid:
    field string<>    // No modifiers applied
    field number<>    // No modifiers applied
    This is equivalent to having no modifiers at all.

    Negation with !#

    Negate a modifier using ! prefix:
    url string<!startsWith:"http://">    // Reject http:// URLs
    code string<!contains:admin>         // Reject if contains "admin"
    value number<!min:0>                 // Negate the min constraint
    Interpretation depends on adapter - some may treat ! as logical negation, others may ignore it.

    Grouped Modifiers with ()#

    Group modifiers to create processing pipelines using parentheses:
    handle string<unique|(lowercase|contains:byte)>
    field string<(group1|group2)|other>
    value string<(transform1|transform2|validate)>
    Behavior:
    Modifiers in a group execute sequentially
    Groups can be nested: <(outer|(inner1|inner2)|outer2)>
    The specific execution semantics depend on your adapter
    Example patterns:
    username string<unique|(lowercase|trim|minLength:3)>
    email string<(trim|lowercase)|isEmail>
    code string<(uppercase|trim)|(length:6|matches:"^[A-Z]+$")>

    Enums with ()#

    Enums define a set of allowed values using parentheses with | separator:
    status string(active|inactive|pending)
    role string(admin|user|guest)
    priority number(1|2|3|4|5)
    size string(small|medium|large)

    Enum Syntax#

    Basic enum:
    field expression(value1|value2|value3)
    Values are separated by | and can be:
    Strings: status(active|inactive)
    Numbers: priority(1|2|3)
    Mixed: value(low|1|high|5) (adapter-dependent interpretation)

    Enum Values with Special Characters#

    When enum values contain the | separator, use quotes:
    mode string(read|write|"read|write")
    separator string(comma|pipe|"pipe|delimited")
    format string(json|xml|"json|xml")

    Empty Enums#

    Empty enum brackets are valid:
    field string()    // No enum constraint
    field number()    // No enum constraint
    This is equivalent to having no enum at all.

    Combining Modifiers and Enums#

    Modifiers and enums can coexist in any order:
    status string<required>(active|inactive)
    status (active|inactive)<required>
    status string(active|inactive)<required>
    All three variations are equivalent.
    Common patterns:
    // Enum with validation
    role string<lowercase>(admin|user|moderator)
    priority number<min:1|max:5>(1|2|3|4|5)
    
    // Enum with transformation
    status string<uppercase|trim>(active|inactive|pending)
    code string<trim>(A|B|C|D)
    
    // Complex combinations
    email string<isEmail|lowercase>(user@example.com|admin@example.com)
    size string<uppercase>(xs|s|m|l|xl)

    Expression Patterns#

    Type-Only Expressions#

    User {
      name string
      age number
      isActive boolean
      data object
      items array
    }

    With Modifiers#

    User {
      email string<isEmail|lowercase>
      age number<min:18|max:120>
      username string<minLength:3|maxLength:20|trim>
      createdAt string<isISO>
    }

    With Enums#

    User {
      status string(active|inactive|suspended)
      role string(admin|user|guest)
      theme string(light|dark|auto)
    }

    With Both Modifiers and Enums#

    User {
      status string<required|lowercase>(active|inactive|pending)
      priority number<min:1|max:3>(1|2|3)
      role string<trim>(admin|user|moderator)
    }

    Grouped Modifiers#

    User {
      handle string<unique|(lowercase|trim|minLength:3)>
      email string<(trim|lowercase)|isEmail>
      code string<(uppercase|trim)|length:6>
    }

    Complex Expressions#

    Product {
      name string<trim|minLength:1|maxLength:200>
      status string<lowercase>(active|inactive|archived)
      price number<min:0|max:1000000>
      sku string<uppercase|unique|length:8>
      category string<trim|(lowercase|minLength:2)>(electronics|clothing|food)
    }

    Examples#

    Validation Schema#

    User {
      id string<ulid>
      email string<isEmail|lowercase|unique>
      username string<minLength:3|maxLength:20|trim>
      password string<minLength:8>
      age number<min:13|max:120>
      role string(user|admin|moderator)
      status string<lowercase>(active|inactive|suspended)
      bio? string<maxLength:500|trim>
      website? string<isUrl>
      tags[]? string<lowercase|trim>
    }

    API Specification#

    GetUserEndpoint {
      method GET
      path /api/users/:id
      
      params {
        id string<ulid>
      }
      
      response {
        status number(200|404|500)
        body object
      }
    }

    Configuration Schema#

    ServerConfig {
      host string<trim>
      port number<min:1|max:65535>
      environment string(development|staging|production)
      logLevel string<uppercase>(DEBUG|INFO|WARN|ERROR)
      enableMetrics boolean
      timeout number<min:1000|max:30000>
      retries number<min:0|max:5>
    }

    Database Schema#

    Product {
      id string<ulid|unique|indexed>
      name string<minLength:1|maxLength:200|indexed>
      sku string<uppercase|unique|indexed>
      price number<min:0>
      quantity number<min:0>
      status string<indexed>(active|inactive|discontinued)
      categoryId string<ulid|indexed>
      createdAt string<isISO|indexed>
    }

    Data Transformation#

    UserInput {
      firstName string<trim|capitalize>
      lastName string<trim|capitalize>
      email string<trim|lowercase>
      phone string<trim>
      zipCode string<trim|uppercase>
      country string<uppercase|length:2>
    }

    Complex Validation#

    Order {
      id string<ulid>
      orderNumber string<unique|uppercase>
      
      items[] {
        productId string<ulid>
        quantity number<min:1|max:100>
        price number<min:0>
      }
      
      status string<lowercase>(pending|confirmed|processing|shipped|delivered|cancelled)
      paymentStatus string<lowercase>(pending|authorized|captured|failed|refunded)
      
      total number<min:0>
      currency string<uppercase|length:3>(USD|EUR|GBP|JPY)
      
      email string<isEmail|lowercase>
      phone? string<trim>
      
      notes? string<maxLength:1000|trim>
    }

    Grouped Modifier Pipelines#

    SocialProfile {
      username string<unique|(lowercase|trim|minLength:3|maxLength:20)>
      displayName string<(trim|capitalize)|minLength:1|maxLength:50>
      bio? string<(trim|stripHtml)|maxLength:500>
      handle string<unique|(lowercase|trim)|(startsWith:@|minLength:4)>
      
      tags[] string<(lowercase|trim)>
      links[]? string<isUrl|(trim|lowercase)>
    }

    Empty Constructs#

    These are all valid in SPECML:
    field string<>        // Empty modifiers (no constraints)
    field string()        // Empty enum (no restriction)
    field                 // No expression (any type)

    Whitespace in Expressions#

    SPECML is flexible with whitespace within expressions:
    // Compact
    age number<min:10|max:100>
    status string(active|inactive)
    
    // Spaced
    age number < min:10 | max:100 >
    status string ( active | inactive )
    
    // Very spaced
    age number  <  min:10  |  max:100  >
    status string  (  active  |  inactive  )
    All variations are equivalent.

    Key Takeaways#

    1.
    Expressions follow declarations - Separated by whitespace
    2.
    Adapter-dependent semantics - SPECML defines syntax, adapters define meaning
    3.
    Modifiers use <> - Add constraints, transformations, or metadata
    4.
    Enums use () - Define allowed values
    5.
    Combine freely - Modifiers and enums can coexist
    6.
    Quote special chars - Use quotes for | and : in arguments
    7.
    Group with () - Create modifier pipelines
    8.
    Negate with ! - Invert modifier logic
    9.
    Empty is valid - Empty modifiers, enums, or expressions are allowed
    10.
    Whitespace flexible - Add spaces for readability
    For common mistakes with expressions, see Appendix: Common Pitfalls.

    Common Patterns#

    This section demonstrates real-world SPECML patterns, progressing from simple to advanced techniques. Examples are particularly focused on fintech and financial services use cases.

    Pattern 1: Basic Entity with Timestamps#

    When to use: Every entity that needs audit trails.
    Technique: Composition with copy operator.
    Timestamps {
      createdAt string<isISO>
      updatedAt string<isISO>
    }
    
    User {
      >Timestamps
      id string<ulid>
      email string<isEmail|lowercase>
      name string
    }
    Why this works:
    Define timestamps once, reuse everywhere
    Consistent timestamp format across your schema
    Easy to extend (add deletedAt, lastModifiedBy, etc.)
    Example data:
    {
      "createdAt": "2024-03-15T10:30:00Z",
      "updatedAt": "2024-03-15T10:30:00Z",
      "id": "01HQXYZ123ABC",
      "email": "user@example.com",
      "name": "John Doe"
    }

    Pattern 2: Soft Delete Pattern#

    When to use: When you need to "delete" records without actually removing them (compliance, audit trails).
    Technique: Optional fields with copy operator.
    Timestamps {
      createdAt string<isISO>
      updatedAt string<isISO>
    }
    
    SoftDelete {
      deletedAt? string<isISO>
      deletedBy? string<ulid>
      isDeleted boolean
    }
    
    Account {
      >Timestamps
      >SoftDelete
      id string<ulid>
      accountNumber string<unique>
      balance number
    }
    Why this works:
    Records are never physically deleted
    Maintain audit trail of who deleted and when
    isDeleted flag for quick filtering
    Optional fields mean active records don't need these fields
    Example data (active account):
    {
      "createdAt": "2024-01-10T08:00:00Z",
      "updatedAt": "2024-03-15T14:22:00Z",
      "isDeleted": false,
      "id": "01ACCOUNT123",
      "accountNumber": "ACC-2024-001",
      "balance": 15000.50
    }
    Example data (deleted account):
    {
      "createdAt": "2024-01-10T08:00:00Z",
      "updatedAt": "2024-03-15T14:22:00Z",
      "deletedAt": "2024-03-15T14:22:00Z",
      "deletedBy": "01USER789",
      "isDeleted": true,
      "id": "01ACCOUNT123",
      "accountNumber": "ACC-2024-001",
      "balance": 0
    }

    Pattern 3: Address Standardization#

    When to use: Any system that handles physical addresses.
    Technique: Reusable reference type with validation.
    Address {
      streetLine1 string<trim|minLength:1|maxLength:100>
      streetLine2? string<trim|maxLength:100>
      city string<trim|minLength:2|maxLength:50>
      state string<trim|length:2|uppercase>
      postalCode string<trim>
      country string<length:2|uppercase>
    }
    
    Customer {
      id string<ulid>
      name string
      billingAddress#Address
      shippingAddress#Address?
      mailingAddress#Address?
    }
    Why this works:
    Single source of truth for address format
    Consistent validation across all addresses
    Optional shipping/mailing addresses for flexibility
    Country codes use ISO 3166-1 alpha-2
    Example data:
    {
      "id": "01CUST456",
      "name": "Jane Smith",
      "billingAddress": {
        "streetLine1": "123 Main Street",
        "streetLine2": "Apt 4B",
        "city": "New York",
        "state": "NY",
        "postalCode": "10001",
        "country": "US"
      },
      "shippingAddress": {
        "streetLine1": "456 Work Plaza",
        "city": "Boston",
        "state": "MA",
        "postalCode": "02101",
        "country": "US"
      }
    }

    Pattern 4: Money and Currency Handling#

    When to use: Financial transactions, pricing, balances - any monetary value.
    Technique: Structured money type with currency validation.
    Money {
      amount number<min:0>
      currency string<uppercase|length:3>(USD|EUR|GBP|JPY|CHF|CAD|AUD)
    }
    
    Transaction {
      id string<ulid>
      accountId string<ulid>
      transactionAmount#Money
      balanceAfter#Money
      timestamp string<isISO>
      type string(debit|credit)
      status string(pending|completed|failed)
    }
    Why this works:
    Amount and currency are always paired
    Prevents currency mismatch errors
    ISO 4217 currency codes
    Minimum zero prevents negative amounts (use type for debit/credit)
    Example data:
    {
      "id": "01TXN789",
      "accountId": "01ACC123",
      "transactionAmount": {
        "amount": 250.50,
        "currency": "USD"
      },
      "balanceAfter": {
        "amount": 15250.50,
        "currency": "USD"
      },
      "timestamp": "2024-03-15T14:30:22Z",
      "type": "credit",
      "status": "completed"
    }

    Pattern 5: KYC (Know Your Customer) Document Management#

    When to use: Compliance, identity verification, onboarding.
    Technique: Enums for document types, optional verification fields.
    Timestamps {
      createdAt string<isISO>
      updatedAt string<isISO>
    }
    
    KYCDocument {
      >Timestamps
      id string<ulid>
      customerId string<ulid>
      documentType string(passport|drivers_license|national_id|utility_bill|bank_statement)
      documentNumber string<trim>
      documentUrl string<isUrl>
      expirationDate? string<isISO>
      verificationStatus string(pending|verified|rejected|expired)
      verifiedAt? string<isISO>
      verifiedBy? string<ulid>
      rejectionReason? string
    }
    Why this works:
    Tracks document lifecycle
    Verification audit trail
    Handles documents with/without expiration
    Clear rejection reason for compliance
    Example data:
    {
      "createdAt": "2024-03-10T09:00:00Z",
      "updatedAt": "2024-03-11T15:30:00Z",
      "id": "01DOC456",
      "customerId": "01CUST789",
      "documentType": "passport",
      "documentNumber": "P1234567",
      "documentUrl": "https://secure-storage.example.com/docs/01DOC456",
      "expirationDate": "2030-05-20T00:00:00Z",
      "verificationStatus": "verified",
      "verifiedAt": "2024-03-11T15:30:00Z",
      "verifiedBy": "01ADMIN123"
    }

    Pattern 6: Tiered Account Structure#

    When to use: Multiple account types with shared base properties and tier-specific features.
    Technique: Base type with selective copying for different tiers.
    Timestamps {
      createdAt string<isISO>
      updatedAt string<isISO>
    }
    
    BaseAccount {
      >Timestamps
      id string<ulid>
      accountNumber string<unique>
      customerId string<ulid>
      balance number
      currency string<uppercase|length:3>(USD|EUR|GBP)
      status string(active|suspended|closed)
      
      // All tiers have these limits
      dailyTransactionLimit number<min:0>
      monthlyTransactionLimit number<min:0>
    }
    
    BasicAccount {
      >BaseAccount
      accountType string(basic)
      interestRate number<min:0|max:2>
      monthlyFee number<min:0>
    }
    
    PremiumAccount {
      >BaseAccount
      accountType string(premium)
      interestRate number<min:0|max:5>
      cashbackRate number<min:0|max:3>
      personalBankerId? string<ulid>
      conciergeAccess boolean
    }
    
    BusinessAccount {
      >BaseAccount
      accountType string(business)
      businessName string
      taxId string
      multiUserAccess boolean
      apiAccessEnabled boolean
      accountManagers[] string<ulid>
    }
    Why this works:
    Shared properties defined once in BaseAccount
    Each tier has tier-specific features
    Type-safe account types via enum
    Easy to add new tiers
    Example data (Premium Account):
    {
      "createdAt": "2024-01-15T10:00:00Z",
      "updatedAt": "2024-03-15T14:00:00Z",
      "id": "01ACC789",
      "accountNumber": "PREM-2024-001",
      "customerId": "01CUST456",
      "balance": 50000,
      "currency": "USD",
      "status": "active",
      "dailyTransactionLimit": 50000,
      "monthlyTransactionLimit": 500000,
      "accountType": "premium",
      "interestRate": 3.5,
      "cashbackRate": 2.0,
      "personalBankerId": "01BANKER123",
      "conciergeAccess": true
    }

    Pattern 7: Transaction Ledger with Double-Entry#

    When to use: Accurate financial record-keeping, audit trails.
    Technique: References with arrays for journal entries.
    Timestamps {
      createdAt string<isISO>
      updatedAt string<isISO>
    }
    
    Money {
      amount number<min:0>
      currency string<uppercase|length:3>(USD|EUR|GBP)
    }
    
    LedgerEntry {
      accountId string<ulid>
      entryType string(debit|credit)
      amount#Money
    }
    
    Transaction {
      >Timestamps
      id string<ulid>
      transactionType string(transfer|payment|deposit|withdrawal|fee)
      description string
      entries#LedgerEntry[2]
      referenceNumber string<unique>
      status string(pending|completed|failed|reversed)
      metadata? object
    }
    Why this works:
    Double-entry bookkeeping enforced (exactly 2 entries)
    Debit and credit always balanced
    Immutable transaction record
    Reference numbers for reconciliation
    Example data:
    {
      "createdAt": "2024-03-15T14:30:00Z",
      "updatedAt": "2024-03-15T14:30:00Z",
      "id": "01TXN999",
      "transactionType": "transfer",
      "description": "Transfer to savings account",
      "entries": [
        {
          "accountId": "01CHECKING123",
          "entryType": "debit",
          "amount": {
            "amount": 1000,
            "currency": "USD"
          }
        },
        {
          "accountId": "01SAVINGS456",
          "entryType": "credit",
          "amount": {
            "amount": 1000,
            "currency": "USD"
          }
        }
      ],
      "referenceNumber": "TXN-2024-03-15-999",
      "status": "completed"
    }

    Pattern 8: Risk Assessment and Scoring#

    When to use: Credit decisions, fraud detection, compliance.
    Technique: Computed scores with factor tracking.
    Timestamps {
      createdAt string<isISO>
      updatedAt string<isISO>
    }
    
    RiskFactor {
      factorName string
      factorValue string
      weight number<min:0|max:1>
      impact string(positive|negative|neutral)
    }
    
    RiskAssessment {
      >Timestamps
      id string<ulid>
      customerId string<ulid>
      assessmentType string(credit|fraud|aml|kyc)
      riskScore number<min:0|max:100>
      riskLevel string(low|medium|high|critical)
      factors#RiskFactor[]
      assessedBy string<ulid>
      validUntil string<isISO>
      notes? string
    }
    Why this works:
    Transparent risk calculation
    Factors tracked for explainability
    Time-bound assessments (validUntil)
    Multiple assessment types supported
    Example data:
    {
      "createdAt": "2024-03-15T10:00:00Z",
      "updatedAt": "2024-03-15T10:00:00Z",
      "id": "01RISK123",
      "customerId": "01CUST789",
      "assessmentType": "credit",
      "riskScore": 72,
      "riskLevel": "medium",
      "factors": [
        {
          "factorName": "credit_history",
          "factorValue": "good",
          "weight": 0.4,
          "impact": "positive"
        },
        {
          "factorName": "debt_to_income",
          "factorValue": "0.35",
          "weight": 0.3,
          "impact": "negative"
        },
        {
          "factorName": "payment_history",
          "factorValue": "excellent",
          "weight": 0.3,
          "impact": "positive"
        }
      ],
      "assessedBy": "01SYSTEM",
      "validUntil": "2024-06-15T10:00:00Z",
      "notes": "Automated credit assessment"
    }

    Pattern 9: Payment Methods with Tokenization#

    When to use: Storing payment information securely.
    Technique: References with optional fields, sensitive data masking.
    Timestamps {
      createdAt string<isISO>
      updatedAt string<isISO>
    }
    
    Address {
      streetLine1 string
      city string
      state string
      postalCode string
      country string<length:2|uppercase>
    }
    
    PaymentMethod {
      >Timestamps
      id string<ulid>
      customerId string<ulid>
      type string(credit_card|debit_card|bank_account|digital_wallet)
      
      // Tokenized/masked data - never store raw card numbers
      token string<unique>
      last4 string<length:4>
      
      // Card-specific fields
      brand? string(visa|mastercard|amex|discover)
      expiryMonth? number<min:1|max:12>
      expiryYear? number<min:2024>
      
      // Bank account fields
      bankName? string
      accountType? string(checking|savings)
      
      billingAddress#Address
      isDefault boolean
      isVerified boolean
      verifiedAt? string<isISO>
    }
    Why this works:
    Never stores sensitive card data
    Tokenization for PCI compliance
    Supports multiple payment types
    Verification status tracked
    Example data:
    {
      "createdAt": "2024-03-01T08:00:00Z",
      "updatedAt": "2024-03-15T10:00:00Z",
      "id": "01PAY456",
      "customerId": "01CUST789",
      "type": "credit_card",
      "token": "tok_1234567890abcdef",
      "last4": "4242",
      "brand": "visa",
      "expiryMonth": 12,
      "expiryYear": 2027,
      "billingAddress": {
        "streetLine1": "123 Main St",
        "city": "New York",
        "state": "NY",
        "postalCode": "10001",
        "country": "US"
      },
      "isDefault": true,
      "isVerified": true,
      "verifiedAt": "2024-03-01T08:30:00Z"
    }

    Pattern 10: Investment Portfolio Management#

    When to use: Wealth management, investment tracking.
    Technique: Nested references with calculated fields.
    Timestamps {
      createdAt string<isISO>
      updatedAt string<isISO>
    }
    
    Money {
      amount number
      currency string<uppercase|length:3>(USD|EUR|GBP)
    }
    
    Asset {
      ticker string<uppercase>
      name string
      assetType string(stock|bond|etf|mutual_fund|crypto|real_estate)
      quantity number<min:0>
      costBasis#Money
      currentValue#Money
      gainLoss#Money
      gainLossPercentage number
    }
    
    Portfolio {
      >Timestamps
      id string<ulid>
      customerId string<ulid>
      portfolioName string
      portfolioType string(individual|joint|retirement|trust)
      
      holdings#Asset[]
      
      totalValue#Money
      totalCostBasis#Money
      totalGainLoss#Money
      
      riskProfile string(conservative|moderate|aggressive)
      lastRebalanced? string<isISO>
    }
    Why this works:
    Complete portfolio view
    Per-asset and total calculations
    Multiple portfolio types
    Rebalancing tracking
    Example data:
    {
      "createdAt": "2024-01-01T00:00:00Z",
      "updatedAt": "2024-03-15T16:00:00Z",
      "id": "01PORT123",
      "customerId": "01CUST456",
      "portfolioName": "Retirement Portfolio",
      "portfolioType": "retirement",
      "holdings": [
        {
          "ticker": "AAPL",
          "name": "Apple Inc.",
          "assetType": "stock",
          "quantity": 100,
          "costBasis": {
            "amount": 15000,
            "currency": "USD"
          },
          "currentValue": {
            "amount": 17500,
            "currency": "USD"
          },
          "gainLoss": {
            "amount": 2500,
            "currency": "USD"
          },
          "gainLossPercentage": 16.67
        },
        {
          "ticker": "VTI",
          "name": "Vanguard Total Stock Market ETF",
          "assetType": "etf",
          "quantity": 200,
          "costBasis": {
            "amount": 40000,
            "currency": "USD"
          },
          "currentValue": {
            "amount": 44000,
            "currency": "USD"
          },
          "gainLoss": {
            "amount": 4000,
            "currency": "USD"
          },
          "gainLossPercentage": 10.0
        }
      ],
      "totalValue": {
        "amount": 61500,
        "currency": "USD"
      },
      "totalCostBasis": {
        "amount": 55000,
        "currency": "USD"
      },
      "totalGainLoss": {
        "amount": 6500,
        "currency": "USD"
      },
      "riskProfile": "moderate",
      "lastRebalanced": "2024-01-01T00:00:00Z"
    }

    Pattern 11: Loan and Credit Facility#

    When to use: Lending, credit lines, mortgages.
    Technique: Complex references with payment schedules.
    Timestamps {
      createdAt string<isISO>
      updatedAt string<isISO>
    }
    
    Money {
      amount number<min:0>
      currency string<uppercase|length:3>(USD|EUR|GBP)
    }
    
    PaymentSchedule {
      dueDate string<isISO>
      principalAmount#Money
      interestAmount#Money
      totalAmount#Money
      status string(upcoming|paid|overdue|missed)
      paidDate? string<isISO>
    }
    
    Loan {
      >Timestamps
      id string<ulid>
      loanNumber string<unique>
      customerId string<ulid>
      
      loanType string(personal|auto|mortgage|business|student)
      principalAmount#Money
      interestRate number<min:0|max:30>
      apr number<min:0|max:40>
      termMonths number<min:1>
      paymentFrequency string(monthly|biweekly|weekly)
      
      currentBalance#Money
      paidToDate#Money
      remainingPayments number<min:0>
      
      originationDate string<isISO>
      firstPaymentDate string<isISO>
      maturityDate string<isISO>
      
      paymentSchedule#PaymentSchedule[]
      
      status string(pending|active|paid_off|defaulted|charged_off)
      collateralDescription? string
      collateralValue? Money
    }
    Why this works:
    Complete loan lifecycle tracking
    Payment schedule visibility
    Collateral tracking for secured loans
    Clear status progression
    Example data:
    {
      "createdAt": "2024-01-15T10:00:00Z",
      "updatedAt": "2024-03-15T14:00:00Z",
      "id": "01LOAN789",
      "loanNumber": "LOAN-2024-001",
      "customerId": "01CUST456",
      "loanType": "auto",
      "principalAmount": {
        "amount": 25000,
        "currency": "USD"
      },
      "interestRate": 5.5,
      "apr": 5.75,
      "termMonths": 60,
      "paymentFrequency": "monthly",
      "currentBalance": {
        "amount": 23500,
        "currency": "USD"
      },
      "paidToDate": {
        "amount": 1500,
        "currency": "USD"
      },
      "remainingPayments": 57,
      "originationDate": "2024-01-15T00:00:00Z",
      "firstPaymentDate": "2024-02-15T00:00:00Z",
      "maturityDate": "2029-01-15T00:00:00Z",
      "paymentSchedule": [
        {
          "dueDate": "2024-02-15T00:00:00Z",
          "principalAmount": {
            "amount": 365.52,
            "currency": "USD"
          },
          "interestAmount": {
            "amount": 114.58,
            "currency": "USD"
          },
          "totalAmount": {
            "amount": 480.10,
            "currency": "USD"
          },
          "status": "paid",
          "paidDate": "2024-02-14T08:00:00Z"
        }
      ],
      "status": "active",
      "collateralDescription": "2023 Toyota Camry",
      "collateralValue": {
        "amount": 28000,
        "currency": "USD"
      }
    }

    Pattern 12: Regulatory Compliance Reporting#

    When to use: SAR filings, CTR reports, audit trails.
    Technique: Selective copying with audit metadata.
    Timestamps {
      createdAt string<isISO>
      updatedAt string<isISO>
    }
    
    AuditMetadata {
      submittedBy string<ulid>
      submittedAt string<isISO>
      reviewedBy? string<ulid>
      reviewedAt? string<isISO>
      approvedBy? string<ulid>
      approvedAt? string<isISO>
    }
    
    Transaction {
      id string<ulid>
      accountId string<ulid>
      amount number
      currency string
      type string
      timestamp string<isISO>
      description string
      counterpartyName? string
      counterpartyAccount? string
    }
    
    ComplianceReport {
      >Timestamps
      >AuditMetadata
      id string<ulid>
      reportType string(sar|ctr|ofac|aml|kyc)
      reportNumber string<unique>
      
      customerId string<ulid>
      customerName string
      
      // Selectively include transaction data
      relatedTransactions#Transaction[]
      
      suspiciousActivity? string
      riskLevel string(low|medium|high|critical)
      
      filedWith? string(fincen|sec|occ|fdic)
      filingReference? string
      filingDate? string<isISO>
      
      status string(draft|under_review|approved|filed|archived)
      notes string
      attachments[]? string<isUrl>
    }
    Why this works:
    Complete audit trail
    Multi-level approval workflow
    Transaction linkage
    External filing tracking
    Example data:
    {
      "createdAt": "2024-03-10T09:00:00Z",
      "updatedAt": "2024-03-15T16:00:00Z",
      "submittedBy": "01ANALYST123",
      "submittedAt": "2024-03-10T09:00:00Z",
      "reviewedBy": "01MANAGER456",
      "reviewedAt": "2024-03-12T14:00:00Z",
      "approvedBy": "01COMPLIANCE789",
      "approvedAt": "2024-03-15T16:00:00Z",
      "id": "01RPT999",
      "reportType": "sar",
      "reportNumber": "SAR-2024-0315-001",
      "customerId": "01CUST567",
      "customerName": "John Doe",
      "relatedTransactions": [
        {
          "id": "01TXN111",
          "accountId": "01ACC222",
          "amount": 12000,
          "currency": "USD",
          "type": "wire",
          "timestamp": "2024-03-08T10:00:00Z",
          "description": "Wire transfer",
          "counterpartyName": "ABC Corp",
          "counterpartyAccount": "9876543210"
        }
      ],
      "suspiciousActivity": "Multiple large wire transfers to high-risk jurisdiction",
      "riskLevel": "high",
      "filedWith": "fincen",
      "filingReference": "FINCEN-SAR-2024-12345",
      "filingDate": "2024-03-15T16:30:00Z",
      "status": "filed",
      "notes": "Customer activity shows structured transactions pattern",
      "attachments": [
        "https://secure.example.com/reports/sar-2024-0315-001.pdf"
      ]
    }

    Pattern 13: Multi-Currency Wallet#

    When to use: Crypto exchanges, forex platforms, international banking.
    Technique: Arrays of references with currency-specific balances.
    Timestamps {
      createdAt string<isISO>
      updatedAt string<isISO>
    }
    
    CurrencyBalance {
      currency string<uppercase|length:3>
      availableBalance number
      pendingBalance number
      totalBalance number
      lastUpdated string<isISO>
    }
    
    Wallet {
      >Timestamps
      id string<ulid>
      customerId string<ulid>
      walletType string(fiat|crypto|mixed)
      balances#CurrencyBalance[]
      defaultCurrency string<uppercase|length:3>
      status string(active|frozen|closed)
    }
    Why this works:
    Multiple currencies in one wallet
    Separate available vs pending balances
    Per-currency balance tracking
    Wallet-level default currency
    Example data:
    {
      "createdAt": "2024-01-01T00:00:00Z",
      "updatedAt": "2024-03-15T14:00:00Z",
      "id": "01WALLET123",
      "customerId": "01CUST456",
      "walletType": "mixed",
      "balances": [
        {
          "currency": "USD",
          "availableBalance": 10000,
          "pendingBalance": 500,
          "totalBalance": 10500,
          "lastUpdated": "2024-03-15T14:00:00Z"
        },
        {
          "currency": "EUR",
          "availableBalance": 5000,
          "pendingBalance": 0,
          "totalBalance": 5000,
          "lastUpdated": "2024-03-15T14:00:00Z"
        },
        {
          "currency": "BTC",
          "availableBalance": 0.5,
          "pendingBalance": 0.1,
          "totalBalance": 0.6,
          "lastUpdated": "2024-03-15T14:00:00Z"
        }
      ],
      "defaultCurrency": "USD",
      "status": "active"
    }

    Pattern 14: Complete Fintech Application#

    When to use: Building a comprehensive financial application.
    Technique: All patterns combined - the ultimate example.
    // ===== Shared Base Types =====
    
    Timestamps {
      createdAt string<isISO>
      updatedAt string<isISO>
    }
    
    SoftDelete {
      deletedAt? string<isISO>
      deletedBy? string<ulid>
      isDeleted boolean
    }
    
    AuditMetadata {
      createdBy string<ulid>
      updatedBy string<ulid>
      lastAccessedAt? string<isISO>
    }
    
    Money {
      amount number
      currency string<uppercase|length:3>(USD|EUR|GBP|JPY)
    }
    
    Address {
      streetLine1 string<trim>
      streetLine2? string<trim>
      city string<trim>
      state string<trim|length:2|uppercase>
      postalCode string<trim>
      country string<length:2|uppercase>
    }
    
    // ===== Customer Management =====
    
    KYCDocument {
      >Timestamps
      id string<ulid>
      documentType string(passport|drivers_license|national_id)
      documentNumber string<trim>
      documentUrl string<isUrl>
      expirationDate? string<isISO>
      verificationStatus string(pending|verified|rejected)
      verifiedAt? string<isISO>
      verifiedBy? string<ulid>
    }
    
    RiskAssessment {
      >Timestamps
      id string<ulid>
      riskScore number<min:0|max:100>
      riskLevel string(low|medium|high)
      assessedBy string<ulid>
      validUntil string<isISO>
    }
    
    Customer {
      >Timestamps
      >SoftDelete
      >AuditMetadata
      id string<ulid>
      customerNumber string<unique>
      
      // Personal info
      firstName string<trim>
      lastName string<trim>
      dateOfBirth string<isISO>
      ssn? string<length:11>
      
      // Contact
      email string<isEmail|lowercase|unique>
      phone string<trim>
      address#Address
      
      // KYC & Compliance
      kycDocuments#KYCDocument[]
      riskAssessment#RiskAssessment?
      
      // Account relationship
      accountIds[] string<ulid>
      
      status string(active|suspended|closed)
      tier string(basic|premium|business)
    }
    
    // ===== Account Management =====
    
    Account {
      >Timestamps
      >SoftDelete
      >AuditMetadata
      id string<ulid>
      accountNumber string<unique>
      customerId string<ulid>
      
      accountType string(checking|savings|investment)
      balance#Money
      availableBalance#Money
      
      interestRate? number<min:0|max:10>
      minimumBalance? Money
      
      status string(active|frozen|closed)
    }
    
    // ===== Transaction Management =====
    
    LedgerEntry {
      accountId string<ulid>
      entryType string(debit|credit)
      amount#Money
    }
    
    Transaction {
      >Timestamps
      >AuditMetadata
      id string<ulid>
      transactionType string(transfer|payment|deposit|withdrawal|fee)
      description string
      
      entries#LedgerEntry[2]
      
      referenceNumber string<unique>
      status string(pending|completed|failed|reversed)
      
      // Compliance flags
      requiresReview? boolean
      reviewedBy? string<ulid>
      reviewedAt? string<isISO>
    }
    
    // ===== Payment Management =====
    
    PaymentMethod {
      >Timestamps
      id string<ulid>
      customerId string<ulid>
      type string(credit_card|debit_card|bank_account)
      
      token string<unique>
      last4 string<length:4>
      brand? string(visa|mastercard|amex|discover)
      
      expiryMonth? number<min:1|max:12>
      expiryYear? number<min:2024>
      
      billingAddress#Address
      isDefault boolean
      isVerified boolean
    }
    
    Payment {
      >Timestamps
      id string<ulid>
      paymentNumber string<unique>
      customerId string<ulid>
      paymentMethodId string<ulid>
      
      amount#Money
      fee? Money
      totalAmount#Money
      
      status string(pending|processing|completed|failed|refunded)
      
      description string
      merchantName? string
      merchantCategory? string
      
      processedAt? string<isISO>
      failureReason? string
    }
    
    // ===== Loan Management =====
    
    PaymentScheduleItem {
      dueDate string<isISO>
      principalAmount#Money
      interestAmount#Money
      totalAmount#Money
      status string(upcoming|paid|overdue|missed)
      paidDate? string<isISO>
    }
    
    Loan {
      >Timestamps
      >AuditMetadata
      id string<ulid>
      loanNumber string<unique>
      customerId string<ulid>
      
      loanType string(personal|auto|mortgage|business)
      principalAmount#Money
      interestRate number<min:0|max:30>
      apr number<min:0|max:40>
      termMonths number<min:1>
      
      currentBalance#Money
      paidToDate#Money
      
      originationDate string<isISO>
      maturityDate string<isISO>
      
      paymentSchedule#PaymentScheduleItem[]
      
      status string(active|paid_off|defaulted)
      collateralValue? Money
    }
    
    // ===== Investment Management =====
    
    Asset {
      ticker string<uppercase>
      name string
      assetType string(stock|bond|etf|crypto)
      quantity number<min:0>
      costBasis#Money
      currentValue#Money
    }
    
    Portfolio {
      >Timestamps
      id string<ulid>
      customerId string<ulid>
      portfolioName string
      portfolioType string(individual|retirement|trust)
      
      holdings#Asset[]
      
      totalValue#Money
      totalCostBasis#Money
      
      riskProfile string(conservative|moderate|aggressive)
    }
    
    // ===== Compliance & Reporting =====
    
    ComplianceReport {
      >Timestamps
      >AuditMetadata
      id string<ulid>
      reportType string(sar|ctr|aml)
      reportNumber string<unique>
      
      customerId string<ulid>
      relatedTransactionIds[] string<ulid>
      
      suspiciousActivity? string
      riskLevel string(low|medium|high|critical)
      
      status string(draft|under_review|filed)
      filedWith? string(fincen|sec)
      filingDate? string<isISO>
    }
    Why this complete pattern works:
    Modular design: Each entity is self-contained
    Consistent patterns: Timestamps, soft delete, audit trails everywhere
    Data integrity: Double-entry ledger, money type prevents errors
    Compliance ready: KYC, risk assessment, reporting built-in
    Scalable: Easy to add new account types, payment methods, etc.
    Audit trail: Every change tracked with who/when
    Type safety: References ensure data consistency
    Real-world ready: Handles edge cases like fees, failures, refunds
    This pattern demonstrates:
    1.
    ✅ Copy operator for shared functionality
    2.
    ✅ References for data relationships
    3.
    ✅ Arrays for one-to-many relationships
    4.
    ✅ Fixed-length arrays for double-entry bookkeeping
    5.
    ✅ Optional fields for conditional data
    6.
    ✅ Enums for constrained values
    7.
    ✅ Modifiers for validation
    8.
    ✅ Selective copying for data privacy
    9.
    ✅ Nested structures for complex data
    10.
    ✅ Transitive copying for layered composition

    Key Takeaways from Common Patterns#

    Composition Over Duplication#

    Always use the copy operator > to share common fields:
    Timestamps {
      createdAt string<isISO>
      updatedAt string<isISO>
    }
    
    // ✅ Good
    User {
      >Timestamps
      name string
    }
    
    // ❌ Avoid
    User {
      createdAt string<isISO>
      updatedAt string<isISO>
      name string
    }

    References for Relationships#

    Use references to link related entities:
    Address {
      street string
      city string
    }
    
    // ✅ Good
    User {
      homeAddress#Address
    }
    
    // ❌ Avoid
    User {
      homeStreet string
      homeCity string
    }

    Money as a Structured Type#

    Always pair amount with currency:
    // ✅ Good
    Money {
      amount number
      currency string<uppercase|length:3>
    }
    
    Transaction {
      total#Money
    }
    
    // ❌ Avoid
    Transaction {
      totalAmount number
      currency string
    }

    Optional for Progressive Data#

    Use optional for data collected over time:
    // ✅ Good - collected progressively
    User {
      email string           // Required upfront
      phone? string          // Can add later
      address#Address?       // Can add later
    }

    Enums for Constrained Values#

    Use enums for known, fixed values:
    // ✅ Good
    status string(active|inactive|suspended)
    
    // ❌ Avoid
    status string  // Any string allowed

    Arrays for Collections#

    Use arrays for one-to-many relationships:
    // ✅ Good
    User {
      addresses#Address[]
    }
    
    // ❌ Avoid
    User {
      address1#Address
      address2#Address
      address3#Address
    }

    Audit Trails Everywhere#

    Include who and when for changes:
    AuditMetadata {
      createdBy string<ulid>
      updatedBy string<ulid>
      lastAccessedAt? string<isISO>
    }
    
    // Use it
    SensitiveData {
      >Timestamps
      >AuditMetadata
      // ... fields
    }

    Soft Delete for Compliance#

    Never physically delete in regulated environments:
    SoftDelete {
      deletedAt? string<isISO>
      deletedBy? string<ulid>
      isDeleted boolean
    }
    
    Account {
      >SoftDelete
      // ... fields
    }

    Next Steps#

    These patterns provide a foundation for building production-ready schemas. Mix and match them based on your needs:
    Start simple: Begin with basic entities and timestamps
    Add complexity gradually: Introduce references, then copy operations
    Think compliance first: Build in audit trails from day one
    Design for change: Use optional fields and extensible patterns
    Leverage composition: Reuse types across your schema
    For more advanced patterns and edge cases, see the examples in each section of this documentation.

    Design Considerations#

    SPECML is intentionally unopinionated about how you structure your schemas. This section presents common considerations and trade-offs that teams have found useful when designing schemas. Adapt these ideas to fit your context - there are no hard rules.
    Every project has unique requirements, constraints, and team preferences. What works for one team may not work for another. Use these considerations as a starting point for discussions, not as mandates.

    Schema Organization#

    Consider Defining Shared Types First#

    Placing commonly referenced types at the beginning of your file can improve readability, even though SPECML supports forward references.
    // Consider this approach
    Timestamps {
      createdAt string<isISO>
      updatedAt string<isISO>
    }
    
    Address {
      street string
      city string
    }
    
    User {
      >Timestamps
      address#Address
    }
    
    // Over this (though both are valid)
    User {
      >Timestamps
      address#Address
    }
    
    Timestamps {
      createdAt string<isISO>
      updatedAt string<isISO>
    }
    
    Address {
      street string
      city string
    }
    Trade-off: Top-to-bottom readability vs. logical grouping by domain concept.

    Consider Grouping Related Types#

    You might find it helpful to group related types together:
    // Payment-related types together
    PaymentMethod {
      // ...
    }
    
    Payment {
      // ...
    }
    
    PaymentSchedule {
      // ...
    }
    
    // Account-related types together
    Account {
      // ...
    }
    
    Transaction {
      // ...
    }
    Trade-off: Domain grouping vs. dependency order vs. alphabetical sorting.

    Naming Conventions#

    Consider Consistent Naming Patterns#

    Consistency in naming can help teams navigate schemas more easily:
    // Consider consistent patterns
    
    // Option 1: Suffixes for collections
    UserList
    ProductCollection
    OrderHistory
    
    // Option 2: Plural names
    Users
    Products
    Orders
    
    // Option 3: Descriptive names
    ActiveUsers
    ArchivedProducts
    PendingOrders
    Trade-off: Different conventions work for different teams. Choose what feels natural for your domain.

    Consider Clarity Over Brevity#

    Descriptive names can make schemas more self-documenting:
    // More descriptive
    userEmailAddress string<isEmail>
    accountCreationTimestamp string<isISO>
    monthlyTransactionLimit number<min:0>
    
    // More concise
    email string<isEmail>
    created string<isISO>
    limit number<min:0>
    Trade-off: Self-documentation vs. conciseness. Consider your team's familiarity with the domain.

    Composition Strategies#

    Consider Copy for Shared Behavior#

    When multiple types share common fields, the copy operator can reduce duplication:
    // With copy operator
    Timestamps {
      createdAt string<isISO>
      updatedAt string<isISO>
    }
    
    User {
      >Timestamps
      name string
    }
    
    Product {
      >Timestamps
      title string
    }
    
    // Without copy operator (more explicit, but duplicative)
    User {
      createdAt string<isISO>
      updatedAt string<isISO>
      name string
    }
    
    Product {
      createdAt string<isISO>
      updatedAt string<isISO>
      title string
    }
    Trade-off: DRY principle vs. explicitness. Some teams prefer seeing all fields inline.

    Consider Selective Copy for Data Privacy#

    Use select or exclude when exposing data to different contexts:
    InternalUser {
      id string
      email string
      passwordHash string
      internalNotes string
    }
    
    // For public API
    PublicUser {
      >InternalUser<exclude:passwordHash,internalNotes>
    }
    
    // For admin dashboard  
    AdminUser {
      >InternalUser<select:id,email,internalNotes>
    }
    Trade-off: Single source of truth vs. independent type definitions.

    Reference Usage#

    Consider References for Reusable Structures#

    When a structure appears in multiple places, references can ensure consistency:
    // With references
    Address {
      street string
      city string
      zipCode string
    }
    
    User {
      homeAddress#Address
      workAddress#Address
    }
    
    // Without references (more flexible, but less consistent)
    User {
      homeAddress {
        street string
        city string
        zipCode string
      }
      workAddress {
        street string
        city string
        zipCode string
      }
    }
    Trade-off: Consistency vs. flexibility. Inline definitions allow field-specific variations.

    Consider When to Use References vs Inline#

    Different approaches suit different scenarios:
    // Reference for reusable, consistent structures
    Contact {
      email string<isEmail>
      phone string
    }
    
    User {
      primaryContact#Contact
      emergencyContact#Contact
    }
    
    // Inline for one-off, context-specific structures
    Order {
      metadata {
        requestId string
        sessionId string
        userAgent string
      }
    }
    Trade-off: Reusability vs. locality. Consider how often the structure is reused.

    Optional Fields#

    Consider Making Fields Optional When Appropriate#

    Optional fields allow progressive data collection:
    // Initial registration
    User {
      email string
      password string
      
      // Can be added later
      firstName? string
      lastName? string
      phone? string
      address#Address?
    }
    Trade-off: Flexibility vs. data completeness. Consider your business requirements.

    Consider Required Fields for Critical Data#

    Some data is always needed for your system to function:
    // Financial transaction - everything required
    Transaction {
      id string<ulid>
      accountId string<ulid>
      amount number
      currency string
      timestamp string<isISO>
      type string(debit|credit)
    }
    Trade-off: Strictness vs. flexibility. Consider what your adapters require.

    Validation and Constraints#

    Consider Modifiers for Data Quality#

    Modifiers can enforce data quality at the schema level:
    User {
      email string<isEmail|lowercase|trim>
      username string<minLength:3|maxLength:20|trim>
      age number<min:13|max:120>
    }
    Trade-off: Schema-level validation vs. application-level validation. Your adapter determines enforcement.

    Consider Enums for Known Values#

    When values are finite and known, enums make intent clear:
    // With enum
    status string(active|inactive|suspended)
    
    // Without enum (more flexible, less safe)
    status string
    Trade-off: Type safety vs. flexibility for future values.

    Arrays and Collections#

    Consider Fixed-Length Arrays When Appropriate#

    Fixed-length arrays document expected structure:
    // RGB color always has 3 values
    rgbColor[3] number<min:0|max:255>
    
    // Coordinates are latitude, longitude
    coordinates[2] number
    
    // Flexible collection
    tags[] string
    Trade-off: Compile-time guarantees vs. runtime flexibility.

    Consider Array Constraints#

    Arrays with at least one element enforce non-empty collections:
    // Must have at least one item
    orderItems#OrderItem[]
    
    // Can be empty using optional
    tags[]? string
    Trade-off: Data integrity vs. empty state handling.

    Audit and Compliance#

    Consider Timestamps for Audit Trails#

    Timestamps help track data lifecycle:
    Timestamps {
      createdAt string<isISO>
      updatedAt string<isISO>
    }
    
    // Most entities
    User {
      >Timestamps
      // ...
    }
    
    Transaction {
      >Timestamps
      // ...
    }
    Trade-off: Audit capability vs. schema simplicity.

    Consider Soft Delete for Compliance#

    In regulated industries, consider preserving deleted records:
    SoftDelete {
      deletedAt? string<isISO>
      deletedBy? string<ulid>
      isDeleted boolean
    }
    
    Account {
      >SoftDelete
      // ...
    }
    Trade-off: Compliance vs. data cleanup complexity.

    Consider Audit Metadata for Accountability#

    Track who made changes:
    AuditMetadata {
      createdBy string<ulid>
      updatedBy string<ulid>
    }
    
    SensitiveData {
      >Timestamps
      >AuditMetadata
      // ...
    }
    Trade-off: Accountability vs. additional data storage.

    Domain Modeling#

    Consider Money as a Structured Type#

    In financial systems, amount and currency should be paired:
    Money {
      amount number
      currency string<uppercase|length:3>
    }
    
    Transaction {
      total#Money
      fee#Money
    }
    Trade-off: Type safety vs. schema simplicity.

    Consider Addresses as Structured Types#

    Reusable address structure ensures consistency:
    Address {
      streetLine1 string
      streetLine2? string
      city string
      state string
      postalCode string
      country string<length:2|uppercase>
    }
    
    User {
      billingAddress#Address
      shippingAddress#Address?
    }
    Trade-off: Consistency vs. locale-specific variations.

    Performance Considerations#

    Consider Selective Copy for Large Types#

    When types are large, selective copying can reduce payload size:
    FullUserProfile {
      // 50+ fields
    }
    
    UserSummary {
      >FullUserProfile<select:id,name,email,avatar>
    }
    Trade-off: Performance vs. maintaining selective lists.

    Consider Nested vs Flat Structures#

    Different structures have different performance characteristics:
    // Nested (more structured)
    User {
      profile {
        firstName string
        lastName string
      }
      settings {
        theme string
        language string
      }
    }
    
    // Flat (simpler queries)
    User {
      firstName string
      lastName string
      theme string
      language string
    }
    Trade-off: Logical grouping vs. query simplicity (adapter-dependent).

    Team Collaboration#

    Consider Comments for Complex Logic#

    Comments explain intent:
    User {
      // Must be at least 13 per COPPA regulations
      age number<min:13>
      
      // Validated against third-party service
      email string<isEmail>
      
      // Optional during registration, required for payout
      taxId? string
    }
    Trade-off: Documentation overhead vs. team understanding.

    Consider Aliases for Legacy Integration#

    Aliases help integrate with existing systems:
    // Modern names in your app
    User {
      id:user_id string
      email:email_address string
      firstName:fname string
    }
    Trade-off: Clean interfaces vs. additional mapping logic.

    Testing and Validation#

    Consider Example Data in Comments#

    Examples help developers understand expected format:
    Transaction {
      // Example: "2024-03-15T14:30:00Z"
      timestamp string<isISO>
      
      // Example: "TXN-2024-03-15-12345"
      referenceNumber string
      
      // Example: {"merchantCategory": "5411", "location": "NY"}
      metadata? object
    }
    Trade-off: Documentation overhead vs. developer clarity.

    Evolution and Change#

    Consider Optional for New Fields#

    When adding fields to existing schemas, consider making them optional for backward compatibility:
    // Version 1
    User {
      email string
      name string
    }
    
    // Version 2 - new field optional
    User {
      email string
      name string
      phoneNumber? string  // New field, optional for backward compatibility
    }
    Trade-off: Backward compatibility vs. data completeness.

    Consider Versioning Strategy#

    Different teams handle schema evolution differently:
    // Option 1: Single schema with optional fields
    User {
      email string
      name string
      v2Feature? string
    }
    
    // Option 2: Versioned schemas
    UserV1 {
      email string
      name string
    }
    
    UserV2 {
      >UserV1
      additionalFeature string
    }
    
    // Option 3: Feature flags
    User {
      email string
      name string
      betaFeature? string
    }
    Trade-off: Evolution strategy depends on your deployment model and adapter capabilities.

    File Organization#

    Consider Single vs Multiple Files#

    SPECML currently supports single-file schemas:
    // Consider organizing within a single file
    
    // ===== Base Types =====
    Timestamps {
      // ...
    }
    
    Address {
      // ...
    }
    
    // ===== User Domain =====
    User {
      // ...
    }
    
    Profile {
      // ...
    }
    
    // ===== Order Domain =====
    Order {
      // ...
    }
    
    OrderItem {
      // ...
    }
    Trade-off: As SPECML evolves to support imports, consider how you'd split files.

    Adapter-Specific Considerations#

    Consider Your Adapter's Requirements#

    Different adapters interpret SPECML differently:
    // For validation adapters
    email string<isEmail|required>
    
    // For API adapters
    method POST
    
    // For database adapters
    id string<ulid|indexed|unique>
    Trade-off: Generic schemas vs. adapter-optimized schemas.

    Consider Testing Against Your Target Adapter#

    Schema correctness depends on your adapter:
    // Valid SPECML, but adapter determines meaning
    customField string<customModifier:value>
    Trade-off: SPECML correctness vs. adapter-specific validation.

    Key Principles#

    As you design schemas, consider these guiding principles:
    1.
    Clarity over cleverness - Clear schemas are maintainable schemas
    2.
    Consistency within context - Be consistent within your project, not dogmatic across projects
    3.
    Document unusual decisions - Use comments to explain non-obvious choices
    4.
    Start simple, evolve - Begin with basic structures and add complexity as needed
    5.
    Adapt to your domain - Financial systems need different patterns than social networks
    6.
    Consider your team - What's intuitive for your team matters more than arbitrary rules
    7.
    Test with your adapter - SPECML syntax is only half the story
    8.
    Optimize when needed - Premature optimization is still premature
    9.
    Version thoughtfully - Plan for change but don't over-engineer
    10.
    Share patterns - Document what works for your team

    Remember#

    These are considerations, not commandments. SPECML's flexibility is its strength. Your domain, team, and requirements should guide your decisions. What works for fintech may not work for e-commerce. What works for startups may not work for enterprises.
    Trust your judgment. Experiment. Iterate. Adapt.

    Summary#

    SPECML (Spec Modeling Language) is a clean, expressive schema description language designed for defining data structures, validation rules, and specifications with maximum flexibility and minimal overhead.

    Core Philosophy#

    SPECML is intentionally unopinionated. It provides powerful syntax for describing data structures while letting adapters determine semantic meaning. Whether you're building validators, API specifications, database schemas, or documentation generators, SPECML gives you the foundation without imposing constraints.

    Key Features#

    1. Line-Based Simplicity#

    Every line has a declaration and optionally an expression, separated by whitespace:
    name string
    age number<min:18>
    status string(active|inactive)
    Clean syntax that's easy to read, write, and parse.

    2. Strict Declaration Ordering#

    Components follow a predictable order: baseName#reference:alias[length]?
    owners#Person:administrators[]?
    Consistency eliminates ambiguity.

    3. Powerful Composition#

    Copy fields from other types using > or ...:
    Timestamps {
      createdAt string<isISO>
      updatedAt string<isISO>
    }
    
    User {
      >Timestamps
      email string
    }
    Build complex schemas from simple, reusable components.

    4. Type References#

    Link to other type definitions with #:
    Address {
      street string
      city string
    }
    
    User {
      homeAddress#Address
      workAddress#Address?
    }
    Ensure consistency across your schema.

    5. Field Aliases#

    Map input names to output names with ::
    User {
      fname:firstName string
      lname:lastName string
      email:emailAddress string
    }
    Perfect for legacy integration and API design.

    6. Flexible Arrays#

    Variable or fixed-length arrays:
    tags[] string                    // At least one element
    coordinates[2] number            // Exactly two elements
    contacts#Person[]?               // Optional array
    Express your data structure precisely.

    7. Optional Fields#

    All fields required by default. Use ? for optional:
    User {
      email string        // Required
      phone? string       // Optional
      bio? string         // Optional
    }
    Explicit optionality improves clarity.

    8. Rich Expressions#

    Modifiers add constraints and metadata:
    email string<isEmail|lowercase>
    age number<min:18|max:120>
    code string<uppercase|length:6>
    Enums restrict values:
    status string(active|inactive|pending)
    role string(admin|user|guest)
    Combine them freely:
    status string<lowercase>(active|inactive|pending)

    9. Selective Copying#

    Copy only what you need:
    FullUser {
      id string
      email string
      passwordHash string
      internalNotes string
    }
    
    PublicUser {
      >FullUser<exclude:passwordHash,internalNotes>
    }
    Privacy and performance in one feature.

    10. Adapter-Agnostic Design#

    SPECML defines syntax. Adapters define semantics:
    Validation adapter: age number means "age must be a number"
    API adapter: method POST means "use POST method"
    Database adapter: id string<unique> means "create unique index"
    One schema language, infinite applications.

    What SPECML Provides#

    Syntax Elements#

    Declarations: Field names with optional components
    Expressions: Types, modifiers, enums, and blocks
    References: Type linking with #
    Aliases: Input/output mapping with :
    Arrays: Collections with [] or [n]
    Optional: Opt-in optionality with ?
    Copy: Composition with > or ...
    Comments: Documentation with //

    Built-In Flexibility#

    Case-sensitive names
    Quoted strings for special characters
    Whitespace-tolerant expressions
    Forward references
    Nested structures
    Transitive copying
    Selective field inclusion/exclusion
    Grouped modifiers
    Modifier negation

    Safety Features#

    Strict declaration ordering
    No circular references
    No self-references
    Limited reference modifications
    Explicit optionality
    Array length constraints

    What SPECML Does NOT Provide#

    SPECML is intentionally minimal:
    ❌ No built-in type system (adapters define types)
    ❌ No execution semantics (adapters determine behavior)
    ❌ No cross-file imports (single-file schemas currently)
    ❌ No nested arrays (arrays of arrays not supported)
    ❌ No runtime validation (adapters handle this)
    ❌ No standard library (adapters provide modifiers)
    This minimalism is a feature, not a bug. SPECML stays out of your way.

    Common Use Cases#

    Data Validation#

    User {
      email string<isEmail|unique>
      age number<min:13|max:120>
      status string(active|inactive)
    }

    API Specifications#

    CreateUserEndpoint {
      method POST
      path /api/users
      body {
        email string<isEmail>
        password string<minLength:8>
      }
    }

    Database Schemas#

    Product {
      id string<ulid|unique|indexed>
      name string<minLength:1|indexed>
      price number<min:0>
      status string(active|discontinued)
    }

    Configuration Files#

    ServerConfig {
      host string
      port number<min:1|max:65535>
      environment string(dev|staging|prod)
      logLevel string(debug|info|warn|error)
    }

    Domain Models#

    Order {
      >Timestamps
      id string<ulid>
      customerId string<ulid>
      items#OrderItem[]
      total#Money
      status string(pending|completed|cancelled)
    }

    Design Patterns#

    Composition#

    Timestamps {
      createdAt string<isISO>
      updatedAt string<isISO>
    }
    
    Entity {
      >Timestamps
      // ... fields
    }

    Soft Delete#

    SoftDelete {
      deletedAt? string<isISO>
      isDeleted boolean
    }
    
    Account {
      >SoftDelete
      // ... fields
    }

    Structured Types#

    Money {
      amount number
      currency string<uppercase|length:3>
    }
    
    Transaction {
      total#Money
    }

    Tiered Types#

    BaseAccount {
      // ... common fields
    }
    
    PremiumAccount {
      >BaseAccount
      // ... premium features
    }

    The SPECML Workflow#

    1.
    Define your schema in .specml files
    2.
    Choose an adapter for your use case
    3.
    Generate artifacts (validators, types, docs, etc.)
    4.
    Use in your application
    schema.specml → adapter → generated code → your app
    SPECML is the single source of truth. Adapters handle the rest.

    Why SPECML?#

    Single Source of Truth#

    Define once, generate many:
    Validators
    Type definitions
    API clients
    Database schemas
    Documentation
    Test fixtures

    Clean Syntax#

    Readable by humans, parseable by machines. No XML verbosity. No JSON Schema complexity. Just clean, expressive declarations.

    Flexibility#

    Adapters can interpret SPECML however they need. No forced opinions. No artificial constraints.

    Composition#

    Build complex schemas from simple, reusable parts. DRY principle applied to schema design.

    Type Safety#

    References ensure consistency. Enums prevent invalid values. Modifiers add constraints.

    Evolution-Friendly#

    Optional fields, aliases, and selective copying make schema evolution manageable.

    Getting Started#

    1.
    Learn the syntax - This documentation covers everything
    2.
    Start simple - Basic fields, then add complexity
    3.
    Use patterns - Leverage common patterns for your domain
    4.
    Pick an adapter - Choose tooling for your use case
    5.
    Iterate - Schemas evolve with your application

    Where to Go Next#

    Common Patterns: Real-world examples, especially for fintech
    Design Considerations: Trade-offs and suggestions (not rules)
    Appendix: Common mistakes and how to avoid them

    Final Thoughts#

    SPECML is a tool, not a framework. It provides syntax and structure without imposing semantics. Whether you're building a fintech platform, an e-commerce site, a social network, or a SaaS application, SPECML adapts to your needs.
    Define your data. Let adapters do the rest.
    Welcome to SPECML. 🚀

    Appendix: Common Pitfalls#

    This appendix catalogs common mistakes when writing SPECML schemas and how to fix them. Use this as a troubleshooting reference when your schema isn't working as expected.

    Declaration Ordering Mistakes#

    ❌ Pitfall 1: Wrong Component Order#

    Problem: Components in the wrong sequence.
    // ❌ WRONG - array before reference
    items[]#Product
    
    // ❌ WRONG - optional before array
    tags?[] string
    
    // ❌ WRONG - alias before reference
    items:products#Product
    
    // ❌ WRONG - optional in the middle
    items[]?#Product
    Solution: Follow the strict ordering: baseName#reference:alias[length]?
    // ✅ CORRECT
    items#Product[]
    tags[]? string
    items#Product:products
    items#Product[]?
    Why it matters: The parser expects components in a specific order. Wrong order = parse error.

    ❌ Pitfall 2: Optional Marker in Expression#

    Problem: Placing ? after the expression starts (after whitespace).
    // ❌ WRONG - ? in expression space
    email string?
    age number<min:18>?
    tags[] string?
    owner#Person? string
    Solution: ? always comes last in the declaration, before whitespace.
    // ✅ CORRECT
    email? string
    age? number<min:18>
    tags[]? string
    owner#Person?
    Why it matters: The ? modifies the declaration (whether the field exists), not the expression (what type it is).

    ❌ Pitfall 3: Multiple Aliases#

    Problem: Trying to chain multiple colons.
    // ❌ WRONG - multiple aliases
    fname:middleName:firstName string
    a:b:c string
    Solution: Use only one alias.
    // ✅ CORRECT
    fname:firstName string
    Why it matters: SPECML supports one input-to-output mapping per field.

    Array Declaration Mistakes#

    ❌ Pitfall 4: Array Brackets Before Reference#

    Problem: Placing [] before #.
    // ❌ WRONG
    phones[]#PhoneNumber
    contacts[5]#Person
    items[]#Product
    Solution: Arrays come after references.
    // ✅ CORRECT
    phones#PhoneNumber[]
    contacts#Person[5]
    items#Product[]
    Why it matters: The ordering rule places arrays in position 4, after references (position 2).

    ❌ Pitfall 5: Array Brackets Before Alias#

    Problem: Placing [] before :.
    // ❌ WRONG
    items[]:products string
    tags[5]:categories string
    Solution: Arrays come after aliases.
    // ✅ CORRECT
    items:products[] string
    tags:categories[5] string
    Why it matters: Aliases are position 3, arrays are position 4.

    ❌ Pitfall 6: Nested Arrays#

    Problem: Trying to create arrays of arrays.
    // ❌ WRONG - not supported
    matrix[][] number
    data[][] string
    Solution: Use alternative structures or flatten.
    // ✅ CORRECT - use nested objects
    Matrix {
      rows[] {
        values[] number
      }
    }
    
    // Or flatten
    flattenedMatrix[] number  // Document that it's row-major order
    Why it matters: SPECML doesn't currently support multi-dimensional arrays.

    Reference Mistakes#

    ❌ Pitfall 7: Reference Without Base Object#

    Problem: Using # without a base name.
    // ❌ WRONG
    >#Person
    ...#User
    >...#Type
    Solution: Always provide a base name for references.
    // ✅ CORRECT
    owner#Person
    >BaseUser
    >config#Settings
    Why it matters: References need a base object to reference from.

    ❌ Pitfall 8: Circular References#

    Problem: Types referencing each other in a loop.
    // ❌ WRONG - circular dependency
    TypeA {
      field#TypeB
    }
    
    TypeB {
      field#TypeA
    }
    Solution: Redesign to avoid circular dependencies.
    // ✅ CORRECT - break the cycle
    Base {
      sharedField string
    }
    
    TypeA {
      field#Base
    }
    
    TypeB {
      field#Base
    }
    Why it matters: SPECML prevents circular references to avoid infinite loops during parsing.

    ❌ Pitfall 9: Self-References#

    Problem: Type referencing itself.
    // ❌ WRONG - self-reference
    TreeNode {
      value string
      children#TreeNode[]
    }
    Solution: Handle recursive structures outside SPECML.
    // ✅ CORRECT - define structure without self-reference
    // Handle recursion in your adapter/application logic
    TreeNode {
      value string
      childIds[] string  // Reference by ID instead
    }
    Why it matters: Self-references create the same issues as circular references.

    ❌ Pitfall 10: Modifying Reference Modifiers#

    Problem: Trying to change modifiers in a reference.
    Person {
      age number<min:18|max:120>
      name string<minLength:1>
    }
    
    // ❌ WRONG - cannot modify modifiers
    User {
      profile#Person {
        age number<min:25>        // Cannot change min value
        name string               // Cannot remove modifiers
      }
    }
    Solution: You can only toggle optionality and narrow enums.
    // ✅ CORRECT
    User {
      profile#Person {
        age?                      // Can make optional
        name                      // Can make required if was optional
      }
    }
    Why it matters: Reference modifications are strictly limited to prevent inconsistencies.

    ❌ Pitfall 11: Adding Fields to References#

    Problem: Adding new fields inside a reference block.
    Person {
      name string
      age number
    }
    
    // ❌ WRONG - cannot add fields
    User {
      profile#Person {
        email string              // Cannot add new field
      }
    }
    Solution: Define new fields outside the reference.
    // ✅ CORRECT
    User {
      profile#Person
      email string                // Add as separate field
    }
    Why it matters: References inherit fields, they don't extend them.

    ❌ Pitfall 12: Extending Reference Enums#

    Problem: Adding new values to a referenced enum.
    Person {
      title string(mr|mrs|miss)
    }
    
    // ❌ WRONG - cannot extend enum
    User {
      contact#Person {
        title string(mr|mrs|miss|dr)  // Cannot add 'dr'
      }
    }
    Solution: Only narrow enums to a subset.
    // ✅ CORRECT
    User {
      contact#Person {
        title string(mr|mrs)      // Subset of original enum
      }
    }
    Why it matters: You can only restrict, not expand, referenced enums.

    Copy Operator Mistakes#

    ❌ Pitfall 13: Copy with Aliases#

    Problem: Trying to alias a copy operation.
    // ❌ WRONG
    User {
      >BaseUser:aliasedUser
      >Timestamps:ts
    }
    Solution: Copy doesn't support aliases.
    // ✅ CORRECT
    User {
      >BaseUser
      >Timestamps
    }
    Why it matters: Copy brings fields into current scope; aliases work on individual fields.

    ❌ Pitfall 14: Copy with Arrays or Optional#

    Problem: Trying to make copy operations arrays or optional.
    // ❌ WRONG
    User {
      >BaseUser[]
      >Timestamps?
      >Config[]?
    }
    Solution: Copy is declaration-only, no arrays or optional markers.
    // ✅ CORRECT
    User {
      >BaseUser
      >Timestamps
      >Config
    }
    Why it matters: Copy merges fields; it doesn't create field instances.

    ❌ Pitfall 15: Copy with Expressions#

    Problem: Adding an expression to a copy operation.
    // ❌ WRONG
    User {
      >BaseUser string
      >Timestamps {
        field string
      }
    }
    Solution: Copy is declaration-only.
    // ✅ CORRECT
    User {
      >BaseUser
      >Timestamps
    }
    Why it matters: Copy copies fields; it doesn't define types.

    ❌ Pitfall 16: Mixing Select and Exclude#

    Problem: Using both select and exclude in the same copy.
    // ❌ WRONG
    User {
      >FullUser<select:name|exclude:email>
      >BaseUser<select:id,email|exclude:password>
    }
    Solution: Use either select OR exclude, not both.
    // ✅ CORRECT
    User {
      >FullUser<select:name>
      >BaseUser<exclude:password>
    }
    Why it matters: Select and exclude are mutually exclusive operations.

    ❌ Pitfall 17: Copy Self-Reference#

    Problem: Type copying from itself.
    // ❌ WRONG
    User {
      >User
      name string
    }
    Solution: Don't copy from yourself.
    // ✅ CORRECT
    BaseUser {
      id string
    }
    
    User {
      >BaseUser
      name string
    }
    Why it matters: Self-copying creates infinite loops.

    ❌ Pitfall 18: Grouped Copy Modifiers#

    Problem: Grouping select/exclude modifiers.
    // ❌ WRONG
    User {
      >BaseUser<(select:name)>
      >FullUser<select:id|(exclude:email)>
    }
    Solution: Don't group copy modifiers.
    // ✅ CORRECT
    User {
      >BaseUser<select:name>
      >FullUser<exclude:email>
    }
    Why it matters: Copy modifiers don't support grouping syntax.

    Expression Mistakes#

    ❌ Pitfall 19: Quotes in Expression Keywords#

    Problem: Quoting type keywords unnecessarily.
    // ❌ WRONG - quotes not needed for type keywords
    age "number"
    name "string"
    Solution: Don't quote type keywords.
    // ✅ CORRECT
    age number
    name string
    Why it matters: Type keywords are not string literals.

    ❌ Pitfall 20: Special Characters Without Quotes#

    Problem: Using | or : in modifier arguments without quotes.
    // ❌ WRONG - special chars need quotes
    pattern string<matches:^[a-z]{2,5}$>
    value string<custom:key:value>
    Solution: Quote arguments with special characters.
    // ✅ CORRECT
    pattern string<matches:"^[a-z]{2,5}$">
    value string<custom:"key:value">
    Why it matters: : and | have special meaning in SPECML syntax.

    Modifier Mistakes#

    ❌ Pitfall 21: Using Brackets in Modifiers#

    Problem: Using () for grouping instead of <>.
    // ❌ WRONG - parentheses are for enums
    email string(isEmail|lowercase)
    Solution: Use angle brackets for modifiers.
    // ✅ CORRECT
    email string<isEmail|lowercase>
    Why it matters: () is for enums, <> is for modifiers.

    ❌ Pitfall 22: Comma-Separated Modifiers#

    Problem: Using commas instead of pipes.
    // ❌ WRONG
    email string<isEmail, lowercase, trim>
    Solution: Use pipes to separate modifiers.
    // ✅ CORRECT
    email string<isEmail|lowercase|trim>
    Why it matters: Pipe | is the modifier separator.

    Enum Mistakes#

    ❌ Pitfall 23: Quotes in Simple Enum Values#

    Problem: Unnecessarily quoting simple enum values.
    // ❌ WRONG - quotes not needed for simple values
    status string("active"|"inactive")
    Solution: Only quote enum values with special characters.
    // ✅ CORRECT
    status string(active|inactive)
    
    // Only quote when needed
    mode string(read|write|"read|write")
    Why it matters: Quotes are only needed for special characters like |.

    ❌ Pitfall 24: Using Angle Brackets for Enums#

    Problem: Using <> for enums instead of ().
    // ❌ WRONG - angle brackets are for modifiers
    status string<active|inactive>
    Solution: Use parentheses for enums.
    // ✅ CORRECT
    status string(active|inactive)
    Why it matters: <> is for modifiers, () is for enums.

    Naming Mistakes#

    ❌ Pitfall 25: Special Characters Without Quotes#

    Problem: Using parser syntax characters in names without quotes.
    // ❌ WRONG - these chars have special meaning
    field:name string
    items[] string
    optional? string
    ref#type string
    Solution: Quote names with parser syntax characters.
    // ✅ CORRECT
    "field:name" string
    "items[]" string
    "optional?" string
    "ref#type" string
    Why it matters: Unquoted :, [], ?, # are interpreted as SPECML syntax.

    ❌ Pitfall 26: Spaces Without Quotes#

    Problem: Using spaces in field names without quotes.
    // ❌ WRONG - space causes lexer split
    First Name string
    User Email string
    Solution: Quote names with spaces.
    // ✅ CORRECT
    "First Name" string
    "User Email" string
    Why it matters: Whitespace separates declaration from expression.

    Validation Mistakes#

    ❌ Pitfall 27: Empty Arrays for Variable Length#

    Problem: Expecting empty arrays to be valid for [].
    tags[] string  // Expects at least one element
    {
      "tags": []  // ❌ Invalid - empty array
    }
    Solution: Use optional arrays if empty is valid.
    // ✅ CORRECT
    tags[]? string  // Can be omitted entirely
    Why it matters: Variable-length arrays require at least one element.

    ❌ Pitfall 28: Null vs Optional Confusion#

    Problem: Thinking optional means nullable.
    email? string
    {
      "email": null  // May or may not be valid depending on adapter
    }
    Solution: Use modifiers for null handling if your adapter supports it.
    // ✅ CORRECT
    email? string<isNonNull>  // Optional but not null
    Why it matters: Optional means "can be omitted," not "can be null."

    Structure Mistakes#

    ❌ Pitfall 29: Multi-line Declarations#

    Problem: Splitting declarations across lines.
    // ❌ WRONG - declarations must be single-line
    email 
      string<isEmail>
      
    user
      #Person
    Solution: Keep declarations on one line.
    // ✅ CORRECT
    email string<isEmail>
    user#Person
    Why it matters: SPECML is line-based; each declaration is one line.

    ❌ Pitfall 30: Standalone Comments Without //#

    Problem: Trying to use comment syntax other than //.
    // ❌ WRONG - only // is supported
    # This is a comment
    /* This is a comment */
    -- This is a comment
    Solution: Use // for comments.
    // ✅ CORRECT
    // This is a comment
    age string // Inline comment
    Why it matters: SPECML only supports // comment syntax.

    Performance Pitfalls#

    ❌ Pitfall 31: Excessive Nesting#

    Problem: Creating deeply nested structures that are hard to work with.
    // ❌ PROBLEMATIC - too deep
    Root {
      level1 {
        level2 {
          level3 {
            level4 {
              level5 {
                actualData string
              }
            }
          }
        }
      }
    }
    Solution: Flatten or use references.
    // ✅ BETTER
    Level5Data {
      actualData string
    }
    
    Root {
      data#Level5Data
    }
    Why it matters: Deep nesting complicates queries and processing.

    ❌ Pitfall 32: Large Copy Without Selection#

    Problem: Copying huge types when you only need a few fields.
    // ❌ INEFFICIENT
    HugeType {
      // 100 fields
    }
    
    SmallType {
      >HugeType  // Copies all 100 fields
    }
    Solution: Use selective copy.
    // ✅ BETTER
    SmallType {
      >HugeType<select:id,name,email>
    }
    Why it matters: Reduces payload size and processing overhead.

    Common Syntax Errors#

    ❌ Pitfall 33: Missing Whitespace#

    Problem: No space between declaration and expression.
    // ❌ WRONG - no space
    namestring
    age#Personnumber
    Solution: Always have whitespace between declaration and expression.
    // ✅ CORRECT
    name string
    age#Person number
    Why it matters: Whitespace is the delimiter between declaration and expression.

    ❌ Pitfall 34: Mismatched Braces#

    Problem: Opening brace without closing or vice versa.
    // ❌ WRONG
    User {
      name string
    // Missing closing brace
    
    Address
      street string
    }
    // Missing opening brace
    Solution: Match every { with a }.
    // ✅ CORRECT
    User {
      name string
    }
    
    Address {
      street string
    }
    Why it matters: Braces must be properly balanced.

    ❌ Pitfall 35: Mismatched Quotes#

    Problem: Opening quote type doesn't match closing.
    // ❌ WRONG
    "Field Name' string
    'Another Field" string
    Solution: Match quote types.
    // ✅ CORRECT
    "Field Name" string
    'Another Field' string
    Why it matters: Quote types must match for proper parsing.

    Quick Reference: Common Fixes#

    ProblemWrongCorrect
    Array before referenceitems[]#Productitems#Product[]
    Optional in expressionemail string?email? string
    Alias before referenceitems:products#Productitems#Product:products
    Multiple aliasesa:b:c stringa:b string
    Copy with alias>BaseUser:user>BaseUser
    Self-referenceType { field#Type }Redesign structure
    Circular referenceA { b#B } B { a#A }Break the cycle
    Nested arraysmatrix[][] numberUse nested objects
    Spaces without quotesFirst Name string"First Name" string
    Empty arraystags[] = []Use tags[]?

    Debugging Checklist#

    When your schema isn't working:
    1.
    ✅ Check declaration component order: baseName#reference:alias[length]?
    2.
    ✅ Verify ? is last in declaration, not in expression
    3.
    ✅ Ensure arrays [] come after # and :
    4.
    ✅ Confirm no circular or self-references
    5.
    ✅ Check for matched braces {}
    6.
    ✅ Check for matched quotes "" or ''
    7.
    ✅ Verify whitespace between declaration and expression
    8.
    ✅ Ensure special characters are quoted
    9.
    ✅ Check modifiers use <> not ()
    10.
    ✅ Check enums use () not <>
    11.
    ✅ Verify no copy operations with aliases/arrays/optional
    12.
    ✅ Confirm no mixing select and exclude in copy

    Getting Help#

    If you encounter an issue not covered here:
    1.
    Review the relevant section in the main documentation
    2.
    Check the Common Patterns section for similar examples
    3.
    Verify your syntax against the examples
    4.
    Test with a minimal reproduction
    5.
    Check your adapter's documentation for adapter-specific issues
    Remember: SPECML provides syntax. Your adapter provides semantics. Some issues may be adapter-specific rather than SPECML syntax errors.
    Modified at 2025-10-08 08:59:56
    Next
    A-SPECML
    Built with