VDL Specification
Overview
Section titled “Overview”VDL is a modern IDL (Interface Definition Language) designed for Schema-First development. It provides a declarative syntax for defining RPC services, data structures, and contracts with strong typing that VDL can interpret and generate code for.
The primary goal of VDL is to offer an intuitive, human-readable format that ensures the best possible developer experience (DX) while maintaining type safety.
This IDL serves as the single source of truth for your projects, from which you can generate type-safe code for multiple programming languages.
VDL Syntax
Section titled “VDL Syntax”This is the syntax for the IDL.
include "./foo.vdl"
// <comment>
/* <multiline comment>*/
""" <Standalone documentation> """
""" <Type documentation> """type <CustomTypeName> { """ <Field documentation> """ <field>[?]: <Type>}
""" <Constant documentation> """const <ConstantName> = <Value>
""" <Enum documentation> """enum <EnumName> { <EnumMember>[ = <EnumValue>] <EnumMember>[ = <EnumValue>]}
""" <Pattern documentation> """pattern <PatternName> = "<PatternValue>"
""" <RPC documentation> """rpc <RPCName> { """ <RPC Standalone documentation> """
""" <Procedure documentation> """ proc <ProcedureName> { input { """ <Field documentation> """ <field>[?]: <PrimitiveType> | <CustomType> }
output { """ <Field documentation> """ <field>[?]: <PrimitiveType> | <CustomType> } }
""" <Stream documentation> """ stream <StreamName> { input { """ <Field documentation> """ <field>[?]: <PrimitiveType> | <CustomType> }
output { """ <Field documentation> """ <field>[?]: <PrimitiveType> | <CustomType> } }}Naming Conventions
Section titled “Naming Conventions”VDL enforces consistent naming conventions to ensure code generated across all target languages is idiomatic and predictable. The built-in formatter will automatically apply these styles to your schema.
| Element | Convention | Example |
|---|---|---|
| Types, Enums, RPC services, Procedures, Streams | PascalCase | UserProfile, GetUser |
| Fields (in types, input, and output blocks) | camelCase | userId, createdAt |
| Constants | UPPER_SNAKE_CASE | MAX_PAGE_SIZE |
| Patterns | PascalCase | UserEventSubject |
| Enum Members | PascalCase | Pending, InProgress |
Note: The formatter will automatically correct casing when you run it on your
.vdlfiles, so you can focus on the logic while the tooling ensures consistency.
Includes
Section titled “Includes”To maintain clean and maintainable projects, VDL allows you to split your schemas into multiple files. This modular approach helps you organize your types and procedures by domain, making them easier to navigate and reuse across different schemas.
How to use Includes
Section titled “How to use Includes”You can include other .vdl files using the include keyword, typically at the top of your file.
type Session { token: string expiresAt: datetime}
// main.vdlinclude "./auth.vdl"
type AuthInfo { session: Session}Core Principles
Section titled “Core Principles”- Flat Context: When a file is included, all its definitions (types, enums, constants, etc.) are merged into the global context. You can think of it as copying the content of the included file into the current file.
- Relative Paths: Includes always use relative paths (e.g.,
./common.vdl) starting from the current file’s directory. - Idempotent Processing: Each file is processed only once. If your project structure leads to the same file being included multiple times, the compiler simply skips files it has already processed, preventing duplication.
This system empowers you to build a robust library of common types while keeping your service-specific logic focused and uncluttered.
Data Types
Section titled “Data Types”Data types are the core components of your schema. They define the precise structure of information exchanged between the client and server, ensuring consistency and type safety across your entire application.
Primitive Types
Section titled “Primitive Types”The VDL IDL provides several built-in primitive types that map directly to standard JSON types while maintaining strong typing.
| Type | JSON Equivalent | Description |
|---|---|---|
string | string | UTF-8 encoded text string. |
int | integer | 64-bit signed integer. |
float | number | 64-bit floating point number. |
bool | boolean | A logical value: true or false. |
datetime | string | An ISO 8601 formatted date and time string. |
Data Structures
Section titled “Data Structures”You can combine primitive and custom types into more complex structures to represent your data accurately.
Arrays
Section titled “Arrays”Represent an ordered collection of elements. All elements in an array must share the same type.
// Syntax: <Type>[]string[] // A list of stringsUser[] // A list of User objectsRepresent a collection of key-value pairs where keys are always strings. Maps are useful for lookups and dynamic dictionaries.
// Syntax: map<<ValueType>>map<int> // Example: { "active": 1, "pending": 5 }map<User> // Example: { "user_123": { ... } }Inline Types (Anonymous Objects)
Section titled “Inline Types (Anonymous Objects)”Define a structure “on the fly” without naming it. This is useful for small, localized data structures that don’t need to be reused elsewhere.
{ latitude: float longitude: float}Custom Types
Section titled “Custom Types”For reusable data structures, you can define named type blocks. These serve as the blueprint for your application’s domain models.
"""Represents a user in the system."""type User { id: string username: string email: string}Type Reuse: Composition & Destructuring
Section titled “Type Reuse: Composition & Destructuring”VDL provides two powerful ways to share fields between types, allowing you to build complex models from simpler ones while avoiding duplication.
1. Composition (Nesting) Include one type as a property of another. This creates a clear hierarchy and relationship between objects.
type AuditMetadata { createdAt: datetime updatedAt: datetime}
type Article { title: string content: string metadata: AuditMetadata // Nested relationship}2. Destructuring (Spreading)
Merge the fields of one type directly into another using the ... operator. This is ideal for “inheriting” fields from a base structure.
type Article { ...AuditMetadata // Fields are flattened into Article title: string content: string}
// Equivalent to:// type Article {// createdAt: datetime// updatedAt: datetime// title: string// content: string// }You can destructure multiple types in a single definition:
type FullEntity { ...AuditMetadata ...OwnershipInfo name: string}Important: Field names must be unique across the entire type. If two destructured types share a field name, or if you define a field that already exists in a destructured type, the compiler will raise an error. You cannot override fields from destructured types.
Field Modifiers
Section titled “Field Modifiers”- Required by Default: All fields are mandatory. The compiler ensures that these fields are present during communication.
- Optional Fields: Use the
?suffix to mark a field as optional.type Profile {bio?: string // This field can be omitted or null}
Documentation
Section titled “Documentation”Adding documentation to your types and fields is highly recommended. These comments are preserved by the compiler and used to generate readable documentation for API consumers.
type Product { """ The unique identifier for the SKU. """ sku: string
""" Optional marketing description. """ description?: string}Constants
Section titled “Constants”Constants allow you to define fixed values that can be referenced throughout your schema and in the generated code. They are useful for configuration values, limits, or any other static data that should be shared across your application.
"""Optional documentation for the constant."""const <ConstantName> = <Value>Constants support the following value types:
- Strings:
const API_VERSION = "v1" - Integers:
const MAX_PAGE_SIZE = 100 - Floats:
const DEFAULT_TAX_RATE = 0.21 - Booleans:
const FEATURE_FLAG_ENABLED = true
""" The maximum number of items allowed per request. """const MAX_ITEMS = 50
""" The current API version string. """const VERSION = "2.1.0"Enumerations
Section titled “Enumerations”Enums define a set of named, discrete values. They are ideal for representing a fixed list of options, such as statuses, categories, or modes. VDL supports two types of enums: string enums and integer enums.
"""Optional documentation for the enum."""enum <EnumName> { <Member1> <Member2>}Enum Type Inference
Section titled “Enum Type Inference”The type of an enum is inferred from the first member’s value:
- If the first member has no explicit value or is assigned a string, the enum is a string enum.
- If the first member is assigned an integer, the enum is an integer enum.
All members within an enum must be of the same type. Mixing string and integer values in a single enum will result in a compiler error.
String Enums
Section titled “String Enums”String enums are the default. If no value is assigned, the member name itself is used as the value. You can also assign explicit string values.
// Implicit values (member name is used as the value)enum OrderStatus { Pending Processing Shipped Delivered Cancelled}
// Explicit string valuesenum HttpMethod { Get = "GET" Post = "POST" Put = "PUT" Delete = "DELETE"}Integer Enums
Section titled “Integer Enums”If the first member is assigned an integer value, the enum becomes an integer enum. All members must have explicit integer values; there is no auto-increment behavior.
enum Priority { Low = 1 Medium = 2 High = 3 Critical = 10}Patterns
Section titled “Patterns”Patterns are template strings that generate helper functions for constructing dynamic string values at runtime. They are particularly useful for defining message queue topics, cache keys, routing paths, or any other string that requires interpolation.
"""Optional documentation for the pattern."""pattern <PatternName> = "<template_string>"Syntax
Section titled “Syntax”A pattern template uses {placeholder} syntax for dynamic segments. Each placeholder becomes a string parameter in the generated function.
""" Generates a NATS subject for user-specific events. """pattern UserEventSubject = "events.users.{userId}.{eventType}"
""" Generates a Redis cache key for a session. """pattern SessionCacheKey = "cache:session:{sessionId}"Generated Code
Section titled “Generated Code”The compiler transforms each pattern into a function that accepts the placeholders as arguments and returns the constructed string. For example, the UserEventSubject pattern above would generate something similar to:
// TypeScriptfunction UserEventSubject(userId: string, eventType: string): string { return `events.users.${userId}.${eventType}`;}// Gofunc UserEventSubject(userId string, eventType string) string { return "events.users." + userId + "." + eventType}This makes patterns a powerful tool for ensuring consistency across your codebase when working with message brokers like NATS, Kafka, or RabbitMQ, or when defining structured cache keys for Redis or Memcached.
RPC Services
Section titled “RPC Services”An rpc block acts as a logical container for your API’s communication endpoints. It allows you to group related Procedures and Streams under a single named service, providing better organization and a clearer structure for your generated clients and server implementation.
"""Optional documentation for the entire service."""rpc <RPCName> { // Procedure and Stream definitions go here}RPC Merging Across Files
Section titled “RPC Merging Across Files”To facilitate large-scale project organization, VDL supports RPC merging. If the same rpc block name is declared in multiple files (for example, via includes), the compiler will automatically merge their contents into a single, unified service.
This allows you to split a large service definition across multiple files by domain or feature:
rpc Users { proc GetUser { ... } proc CreateUser { ... }}
// users_streams.vdlrpc Users { stream UserStatusUpdates { ... }}
// main.vdlinclude "./users_procs.vdl"include "./users_streams.vdl"
// The "Users" RPC now contains GetUser, CreateUser, and UserStatusUpdates.Important: While RPC blocks are merged, duplicate procedure or stream names within the same RPC will cause a compiler error. Each endpoint name must be unique within its service.
Procedures (proc)
Section titled “Procedures (proc)”Procedures are the standard way to define request-response interactions. They represent discrete actions that a client can trigger on the server. They must be defined inside an rpc block.
rpc <RPCName> { """ Describes the purpose of this procedure. """ proc <ProcedureName> { input { """ Field-level documentation. """ <field>: <Type> }
output { """ Field-level documentation. """ <field>: <Type> } }}Streams (stream)
Section titled “Streams (stream)”Streams enable real-time, unidirectional communication from the server to the client using Server-Sent Events (SSE). They are designed for scenarios where the server needs to push updates as they happen. They must be defined inside an rpc block.
rpc <RPCName> { """ Describes the nature of the events being streamed. """ stream <StreamName> { input { """ Subscription parameters. """ <field>: <Type> }
output { """ Event data structure. """ <field>: <Type> } }}Input and Output Blocks
Section titled “Input and Output Blocks”The input and output blocks in procedures and streams behave exactly like inline types. This means they support all the same features, including field documentation and destructuring with the ... operator.
This is particularly useful for sharing common request or response fields across multiple endpoints:
type PaginationParams { page: int limit: int}
type PaginatedResponse { totalItems: int totalPages: int}
rpc Articles { proc ListArticles { input { ...PaginationParams filterByAuthor?: string }
output { ...PaginatedResponse items: Article[] } }}Service Example
Section titled “Service Example”Grouping related functionality makes your schema easier to maintain:
rpc Messaging { """ Sends a new message to a specific channel. """ proc SendMessage { input { channelId: string text: string } output { messageId: string sentAt: datetime } }
""" Real-time feed of messages for a channel. """ stream NewMessages { input { channelId: string } output { sender: string text: string timestamp: datetime } }}Documentation
Section titled “Documentation”Docstrings
Section titled “Docstrings”Docstrings can be used in two ways: associated with specific elements or as standalone documentation.
1. Associated Docstrings
These are placed immediately before an element definition and provide specific documentation for that element. Associated docstrings can be used with: type, rpc, proc, stream, enum, const, pattern, and individual fields.
"""This is documentation for MyType."""type MyType { """ This is documentation for myField. """ myField: string}2. Standalone Docstrings
These provide general documentation for the schema (or an RPC block) and are not associated with any specific element. To create a standalone docstring, ensure there is at least one blank line between the docstring and any following element.
Standalone docstrings can be placed at the schema level (outside of any block) or inside an rpc block. When inside an RPC, they become part of that service’s documentation, which is useful for adding section headers or contextual notes for a group of endpoints.
"""# WelcomeThis is general documentation for the entire schema."""
type MyType { // ...}
rpc MyService { """ # User Management The following procedures handle user lifecycle operations. """
proc CreateUser { ... } proc DeleteUser { ... } proc CreateSession { ... }}Multi-line Docstrings and Indentation
Section titled “Multi-line Docstrings and Indentation”Docstrings support Markdown syntax, allowing you to format your documentation with headings, lists, code blocks, and more.
Since docstrings can contain Markdown, whitespace is significant for formatting constructs like lists or code blocks. To prevent conflicts with VDL’s own syntax indentation, VDL automatically normalizes multi-line docstrings.
The leading whitespace from the first non-empty line is considered the baseline indentation. This baseline is then removed from every line in the docstring. This process preserves the relative indentation, ensuring that Markdown formatting remains intact regardless of how the docstring block is indented in the source file.
Example:
In the following docstring, the first line has 4 spaces of indentation, which will be removed from all lines.
type MyType { """ This is a multi-line docstring.
The list below will be rendered correctly:
- Level 1 - Level 2 """ field: string}The resulting content for rendering will be:
This is a multi-line docstring.
The list below will be rendered correctly:
- Level 1 - Level 2Remember to keep your documentation up to date with your schema changes.
External Documentation Files
Section titled “External Documentation Files”For extensive documentation, you can reference external Markdown files instead of writing content inline. When a docstring contains only a valid path to a .md file, the compiler will read the content of that file and use it as the documentation.
Important: External file paths must always be relative to the .vdl file that references them. If the specified file does not exist, the compiler will raise an error.
// Standalone documentation from external files""" ./docs/welcome.md """""" ./docs/authentication.md """
rpc Users { // Associated documentation from an external file """ ./docs/create-user.md """ proc CreateUser { // ... }}This approach helps maintain clean and focused schema files while allowing for detailed, long-form documentation in separate files. Remember to keep external documentation files up to date with your schema changes.
Deprecation
Section titled “Deprecation”VDL provides a mechanism to mark elements as deprecated, signaling to API consumers that a feature should no longer be used in new code and may be removed in a future version. This applies to type, rpc, proc, stream, enum, const, and pattern definitions.
Basic Deprecation
Section titled “Basic Deprecation”To mark an element as deprecated without a specific message, use the deprecated keyword directly before its definition:
deprecated type LegacyUser { // ...}
deprecated rpc OldService { // ...}
deprecated enum OldStatus { // ...}
deprecated const OLD_LIMIT = 100
deprecated pattern OldQueueName = "legacy.{id}"
rpc MyService { deprecated proc FetchData { // ... }
deprecated stream OldStream { // ... }}Deprecation with Message
Section titled “Deprecation with Message”To provide additional context, such as a migration path or a removal timeline, include a message in parentheses:
deprecated("Use UserV2 instead")type LegacyUser { // ...}
deprecated("This service will be removed in v3.0. Migrate to NewService.")rpc OldService { // ...}Placement
Section titled “Placement”The deprecated keyword must be placed between any associated docstring and the element definition:
"""Original documentation for the type."""deprecated("Use NewType instead")type MyType { // type definition}Effects
Section titled “Effects”Deprecated elements will:
- Be visually flagged in the generated playground and documentation.
- Generate warning comments in the output code to alert developers.
- Continue to function normally in the generated code; deprecation is purely informational.
Complete Example
Section titled “Complete Example”The following example demonstrates a comprehensive schema that uses all the features of the VDL IDL, including includes, constants, enums, patterns, types with composition and destructuring, and RPC services with procedures and streams.
include "./foo.vdl"
// ============================================================================// External Documentation// ============================================================================""" ./docs/welcome.md """""" ./docs/authentication.md """
// ============================================================================// Constants// ============================================================================""" Maximum number of items returned in a single page. """const MAX_PAGE_SIZE = 100
""" Current API version. """const API_VERSION = "1.0.0"
// ============================================================================// Enumerations// ============================================================================""" Represents the status of an order in the system. """enum OrderStatus { Pending Processing Shipped Delivered Cancelled}
""" Priority levels for support tickets. """enum Priority { Low = 1 Medium = 2 High = 3 Critical = 10}
// ============================================================================// Patterns// ============================================================================""" Generates a NATS subject for product-related events. """pattern ProductEventSubject = "events.products.{productId}.{eventType}"
""" Generates a Redis cache key for a user session. """pattern SessionCacheKey = "cache:session:{sessionId}"
// ============================================================================// Shared Types// ============================================================================"""Common fields for all auditable entities."""type AuditMetadata { id: string createdAt: datetime updatedAt: datetime}
"""Standard pagination parameters for list requests."""type PaginationParams { page: int limit: int}
"""Standard pagination response metadata."""type PaginatedResponse { totalItems: int totalPages: int currentPage: int}
// ============================================================================// Domain Types// ============================================================================"""Represents a product in the catalog."""type Product { ...AuditMetadata
""" The name of the product. """ name: string
""" The price of the product in USD. """ price: float
""" The current order status for this product listing. """ status: OrderStatus
""" The date when the product will be available. """ availabilityDate: datetime
""" A list of tags for categorization. """ tags?: string[]}
"""Represents a customer review for a product."""type Review { """ The rating, from 1 to 5. """ rating: int
""" The customer's written feedback. """ comment: string
""" The ID of the user who wrote the review. """ userId: string}
// ============================================================================// RPC Services// ============================================================================"""Catalog Service
Provides operations for managing products and browsing the catalog."""rpc Catalog { """ # Product Lifecycle Endpoints for creating and managing products. """
""" Creates a new product in the system. """ proc CreateProduct { input { product: Product }
output { success: bool productId: string } }
""" Retrieves a product by its ID, including its reviews. """ proc GetProduct { input { productId: string }
output { product: Product reviews: Review[] } }
""" Lists all products with pagination support. """ proc ListProducts { input { ...PaginationParams filterByStatus?: OrderStatus }
output { ...PaginatedResponse items: Product[] } }}
"""Chat Service
Provides real-time messaging capabilities."""rpc Chat { """ Sends a message to a chat room. """ proc SendMessage { input { """ The ID of the chat room. """ chatId: string
""" The content of the message. """ message: string }
output { """ The ID of the created message. """ messageId: string
""" The server timestamp of when the message was recorded. """ timestamp: datetime } }
""" Subscribes to new messages in a specific chat room. """ stream NewMessage { input { chatId: string }
output { id: string message: string userId: string timestamp: datetime } }}Limitations
Section titled “Limitations”The VDL IDL is designed to be simple and focused. As such, there are a few constraints to be aware of:
- Validation Logic: The compiler handles type checking and ensures required fields are present. Any additional business validation logic (e.g., “rating must be between 1 and 5”) must be implemented in your application code.