ZAP C++

Schema Language

Complete reference for the ZAP schema language

Schema Language

The ZAP schema language defines the structure of your messages. Schemas are compiled to generate type-safe code for your target language.

File Structure

Every schema file must start with a unique 64-bit ID:

# Generate a unique ID
zap id
 
# Use it in your schema
@0x85150b117366d14b;

The ID ensures that different schemas with the same type names do not conflict.

Primitive Types

TypeDescriptionC++ TypeSize
VoidNo datazap::Void0 bits
BoolBooleanbool1 bit
Int8Signed 8-bitint8_t8 bits
Int16Signed 16-bitint16_t16 bits
Int32Signed 32-bitint32_t32 bits
Int64Signed 64-bitint64_t64 bits
UInt8Unsigned 8-bituint8_t8 bits
UInt16Unsigned 16-bituint16_t16 bits
UInt32Unsigned 32-bituint32_t32 bits
UInt64Unsigned 64-bituint64_t64 bits
Float3232-bit floatfloat32 bits
Float6464-bit floatdouble64 bits
TextUTF-8 stringzap::Textpointer
DataByte arrayzap::Datapointer

Structs

Structs are the primary composite type:

struct Person {
  name @0 :Text;
  birthdate @1 :Date;
  email @2 :Text;
  phones @3 :List(PhoneNumber);
}
 
struct Date {
  year @0 :Int16;
  month @1 :UInt8;
  day @2 :UInt8;
}
 
struct PhoneNumber {
  number @0 :Text;
  type @1 :Type;
 
  enum Type {
    mobile @0;
    home @1;
    work @2;
  }
}

Field Numbers

Each field has a number (@0, @1, etc.) that identifies it in the binary format. These numbers:

  • Must be unique within a struct
  • Must start at 0 and be sequential (no gaps in new schemas)
  • Cannot be reused even after a field is removed
  • Determine wire format order, not source order

Default Values

Fields can have default values:

struct Config {
  timeout @0 :UInt32 = 30;
  retries @1 :UInt8 = 3;
  host @2 :Text = "localhost";
  enabled @3 :Bool = true;
  ports @4 :List(UInt16) = [80, 443];
}

Default values are encoded as XOR with the actual value, so defaults of zero take no space.

Enums

Enumerations define a set of named values:

enum Color {
  red @0;
  green @1;
  blue @2;
}
 
struct Pixel {
  x @0 :UInt16;
  y @1 :UInt16;
  color @2 :Color;
}

Enum values also have ordinal numbers that cannot be reused.

Lists

Lists hold zero or more elements of a single type:

struct Document {
  title @0 :Text;
  pages @1 :List(Page);
  tags @2 :List(Text);
  scores @3 :List(Float64);
  matrix @4 :List(List(Int32));  # Nested lists
}
 
struct Page {
  content @0 :Text;
}

Unions

Unions allow a field to hold one of several types:

Anonymous Unions

struct Shape {
  union {
    circle @0 :Circle;
    rectangle @1 :Rectangle;
    triangle @2 :Triangle;
  }
}
 
struct Circle {
  radius @0 :Float64;
}
 
struct Rectangle {
  width @0 :Float64;
  height @1 :Float64;
}
 
struct Triangle {
  base @0 :Float64;
  height @1 :Float64;
}

Named Unions

struct Value {
  name @0 :Text;
 
  data :union {
    intValue @1 :Int64;
    floatValue @2 :Float64;
    textValue @3 :Text;
    boolValue @4 :Bool;
  }
}

Union with Void

Use Void for union members that carry no data:

struct Result {
  union {
    success @0 :Data;
    notFound @1 :Void;
    permissionDenied @2 :Void;
    error @3 :Text;
  }
}

Groups

Groups organize fields without adding pointer indirection:

struct Person {
  name @0 :Text;
 
  address :group {
    street @1 :Text;
    city @2 :Text;
    zipCode @3 :Text;
    country @4 :Text;
  }
}

Groups are purely organizational - they are stored inline in the parent struct.

Interfaces (RPC)

Interfaces define RPC methods:

interface Calculator {
  add @0 (a :Int32, b :Int32) -> (result :Int32);
  subtract @1 (a :Int32, b :Int32) -> (result :Int32);
  multiply @2 (a :Int32, b :Int32) -> (result :Int32);
  divide @3 (a :Int32, b :Int32) -> (result :Int32, remainder :Int32);
}
 
interface Database {
  get @0 (key :Text) -> (value :Data);
  put @1 (key :Text, value :Data) -> ();
  delete @2 (key :Text) -> ();
  list @3 (prefix :Text) -> (keys :List(Text));
}

Returning Capabilities

Methods can return interface references (capabilities):

interface Session {
  login @0 (username :Text, password :Text) -> (user :User);
}
 
interface User {
  getName @0 () -> (name :Text);
  getProfile @1 () -> (profile :Profile);
  logout @2 () -> ();
}
 
struct Profile {
  email @0 :Text;
  avatar @1 :Data;
}

Streaming

Use -> stream for methods that return multiple responses:

interface DataStream {
  # Called for each chunk
  write @0 (data :Data) -> stream;
 
  # Called when done
  done @1 () -> ();
}

Generics

Schemas support generic (parameterized) types:

struct Map(Key, Value) {
  entries @0 :List(Entry);
 
  struct Entry {
    key @0 :Key;
    value @1 :Value;
  }
}
 
struct StringIntMap {
  data @0 :Map(Text, Int64);
}
 
# Generic interface
interface Factory(T) {
  create @0 () -> (instance :T);
}

Imports

Schemas can import types from other files:

# common.zap
@0x85150b117366d14b;
 
struct Timestamp {
  seconds @0 :Int64;
  nanos @1 :UInt32;
}
# main.zap
@0xc8b1a9f4e2d3b6a7;
 
using Common = import "common.zap";
 
struct Event {
  name @0 :Text;
  timestamp @1 :Common.Timestamp;
}

Import Paths

# Relative import
using Foo = import "subdir/foo.zap";
 
# Absolute import (from include path)
using Bar = import "/bar.zap";

Annotations

Annotations attach metadata to schema elements:

# Define an annotation
annotation deprecated(field, struct, enum) :Text;
annotation doc(field, struct, interface, method) :Text;
 
# Use annotations
struct OldApi {
  newField @0 :Text;
  oldField @1 :Text $deprecated("Use newField instead");
}
 
struct Person $doc("Represents a person in the system") {
  name @0 :Text $doc("Full legal name");
  age @1 :UInt8;
}

Annotation Targets

Annotations can target:

  • file - The schema file itself
  • struct - Struct definitions
  • field - Struct fields
  • union - Union definitions
  • group - Group definitions
  • enum - Enum definitions
  • enumerant - Enum values
  • interface - Interface definitions
  • method - Interface methods
  • param - Method parameters
  • annotation - Other annotations
  • const - Constants
  • * - Any target

Constants

Define compile-time constants:

const maxSize :UInt32 = 1048576;
const defaultHost :Text = "localhost";
const defaultPorts :List(UInt16) = [80, 443, 8080];
 
const defaultConfig :Config = (
  timeout = 60,
  retries = 5,
  host = "api.example.com"
);

Schema Evolution

Safe Changes (Backward Compatible)

  • Adding new fields (with new, higher ordinal numbers)
  • Renaming fields or types (names are not in the wire format)
  • Adding new enum values at the end
  • Adding new union members
  • Adding new methods to interfaces
  • Changing a field from non-optional to optional

Breaking Changes (Avoid)

  • Removing fields (mark as deprecated instead)
  • Changing field types
  • Reusing field numbers
  • Changing field numbers
  • Removing enum values
  • Reordering enum values
  • Changing method signatures

Deprecation Pattern

struct Person {
  name @0 :Text;
  email @1 :Text;
 
  # Deprecated - use 'email' instead
  emailAddress @2 :Text $deprecated("Use email field");
}

Best Practices

  1. Use meaningful field numbers - Group related fields with consecutive numbers
  2. Reserve field numbers - Leave gaps for future additions in related groups
  3. Document with annotations - Add $doc annotations for complex fields
  4. Use enums for fixed sets - Better than strings for known values
  5. Prefer structs over unions - Unless data is truly mutually exclusive
  6. Keep interfaces focused - Small interfaces are easier to implement and mock

Next Steps