Command Line Interface (CLI)
Check Commands
Constraints
The check constraints command validates your GraphQL schema to ensure correct usage of custom directives and naming conventions. This helps maintain consistency and catch errors early in the development process.
Usage
s2dm check constraints -s <schema_path>Options
-s, --schema PATH: GraphQL schema file or directory containing schema files (required, can be specified multiple times)--naming-config PATH: YAML file containing naming configuration to validate against (optional)
Validation Checks
The command performs the following validations:
- instanceTag Field and Object Rules: Validates proper usage of
@instanceTagdirective andinstanceTagfields - @range Directive: Ensures
minvalue is less than or equal tomaxvalue - @cardinality Directive: Ensures
minvalue is less than or equal tomaxvalue - Naming Conventions (optional): When
--naming-configis provided, validates that type names, field names, enum values, and other elements follow the specified naming conventions
Examples
Basic Validation
Check a single schema file for directive constraint violations:
s2dm check constraints -s schema.graphqlValidate with Naming Configuration
Ensure your schema follows naming conventions defined in a YAML config file:
s2dm check constraints -s schema.graphql --naming-config naming.yamlSee Naming Configuration for details on the naming configuration format.
Compose Command
The compose command merges multiple GraphQL schema files into a single unified schema file. It automatically adds @reference directives to track which file each type was obtained from.
Basic Usage
s2dm compose -s <schema1> -s <schema2> -o <output_file>Options
-s, --schema PATH: GraphQL schema file or directory (required, can be specified multiple times)-r, --root-type TEXT: Root type name for filtering the schema (optional)-q, --selection-query PATH: GraphQL query file for filtering schema based on selected fields (optional)-n, --naming-config PATH: YAML file with naming configuration for transforming type and field names (optional)-e, --expanded-instances: Transform instance tag arrays into nested structures (optional)-o, --output FILE: Output file path (required)
Examples
Compose Multiple Schema Files
Merge multiple GraphQL schema files into a single output:
s2dm compose -s schema1.graphql -s schema2.graphql -o composed.graphqlCompose from Directories
Merge all .graphql files from multiple directories:
s2dm compose -s ./schemas/vehicle -s ./schemas/person -o composed.graphqlReference Directives
The compose command automatically adds @reference(source: String!) directives to all types to track their source:
type Vehicle @reference(source: "schema1.graphql") {
id: ID!
name: String
}
type Person @reference(source: "schema2.graphql") {
id: ID!
name: String
}Types from the S2DM specification (common types, scalars, directives) are marked with:
type InCabinArea2x2 @instanceTag @reference(source: "S2DM Spec") {
row: TwoRowsInCabinEnum
column: TwoColumnsInCabinEnum
}Note: If a type already has a @reference directive in the source schema, it will be preserved and not overwritten.
Filter by Root Type
See Root Type Filtering for details.
s2dm compose -s schema.graphql --root-type Vehicle -o filtered.graphqlFilter by Selection Query
See Selection Query Filtering for details.
s2dm compose -s schema.graphql -q query.graphql -o filtered.graphqlWith Naming Configuration
See Naming Configuration for details.
s2dm compose -s schema.graphql -n naming.yaml -o output.graphqlWith Expanded Instances
See Expanded Instances for details.
s2dm compose -s schema.graphql --expanded-instances -o output.graphqlExport Commands
JSON Schema
This exporter translates the given GraphQL schema to JSON Schema format.
Key Features
- Complete GraphQL Type Support: Handles all GraphQL types including scalars, objects, enums, unions, interfaces, and lists
- Selection Query: Use the
--selection-queryflag to specify which types and fields to export via a GraphQL query. See Selection Query Filtering for more details. - Root Type Filtering: Use the
--root-typeflag to export only a specific type and its dependencies - Naming Configuration: Use the
--naming-configflag to transform type and field names during export. See Naming Configuration for more details. - Expanded Instance Tags: Use the
--expanded-instancesflag to transform instance tag arrays into nested object structures - Strict Nullability Mode: Use the
--strictflag to enforce GraphQL nullability in JSON Schema validation - Directive Support: Converts S2DM directives like
@cardinality,@range, and@noDuplicatesto JSON Schema constraints - Reference-based Output: Uses JSON Schema
$reffor type references, creating clean and maintainable schemas
Example Transformation
Consider the following GraphQL schema:
directive @instanceTag on OBJECT
directive @metadata(comment: String, vssType: String) on FIELD_DEFINITION | OBJECT
type Vehicle @metadata(comment: "Vehicle entity", vssType: "branch") {
id: ID!
door: Door!
}
type Door {
locked: Boolean!
instanceTag: InCabinArea2x3
}
enum TwoRowsInCabinEnum {
ROW1
ROW2
}
enum ThreeColumnsInCabinEnum {
DRIVERSIDE
MIDDLE
PASSENGERSIDE
}
type InCabinArea2x3 @instanceTag {
row: TwoRowsInCabinEnum
column: ThreeColumnsInCabinEnum
}The JSON Schema exporter with --expanded-instances produces:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"Vehicle": {
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
},
"Door": {
"$ref": "#/$defs/Door_Row"
}
},
"type": "object",
"$comment": "Vehicle entity",
"x-metadata": {
"vssType": "branch"
},
"required": [
"id",
"Door"
]
},
"Door": {
"additionalProperties": false,
"properties": {
"locked": {
"type": "boolean"
}
},
"type": "object",
"required": [
"locked"
]
},
"Door_Row": {
"additionalProperties": false,
"properties": {
"ROW1": {
"$ref": "#/$defs/Door_Column"
},
"ROW2": {
"$ref": "#/$defs/Door_Column"
}
},
"type": "object"
},
"Door_Column": {
"additionalProperties": false,
"properties": {
"DRIVERSIDE": {
"$ref": "#/$defs/Door"
},
"MIDDLE": {
"$ref": "#/$defs/Door"
},
"PASSENGERSIDE": {
"$ref": "#/$defs/Door"
}
},
"type": "object"
}
},
"title": "Vehicle",
"$ref": "#/$defs/Vehicle"
}Root Type Filtering
Use the --root-type flag to export only a specific type and its dependencies:
s2dm export jsonschema --schema schema.graphql --output vehicle.json --root-type VehicleThis creates a JSON Schema that references the Vehicle type as the root:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Vehicle",
"$ref": "#/$defs/Vehicle",
"$defs": {
"Vehicle": { ... },
"Engine": { ... },
"FuelType": { ... }
}
}Directive Support
S2DM directives are converted to JSON Schema constraints:
@cardinality(min: 1, max: 5)→"minItems": 1, "maxItems": 5@range(min: 0.0, max: 100.0)→"minimum": 0.0, "maximum": 100.0@noDuplicates→"uniqueItems": true@metadata(comment: "Description", vssType: "branch")→"$comment": "Description", "x-metadata": {"vssType": "branch"}- Custom directives →
"x-directiveName": trueor"x-directiveName": {...}
Strict Nullability Mode
The --strict flag enforces GraphQL field nullability in the resulting JSON Schema:
s2dm export jsonschema --schema schema.graphql --output schema.json --strictExamples
Given this GraphQL schema:
type Vehicle {
id: ID! # Non-null
description: String # Nullable
year: Int # Nullable
category: VehicleCategory # Nullable enum
parts: [Part] # Nullable list of nullable parts
doors: [Door!]! # Non-null list of non-null doors
wheels: [Wheel]! # Non-null list of nullable wheels
}
enum VehicleCategory {
CAR
TRUCK
}Default mode produces:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"Vehicle": {
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
},
"description": {
"type": "string"
},
"category": {
"$ref": "#/$defs/VehicleCategory"
},
"doorsOptional": {
"type": "array",
"items": {
"$ref": "#/$defs/Door"
}
},
"doorsRequired": {
"type": "array",
"items": {
"$ref": "#/$defs/Door"
}
},
"doors": {
"type": "array",
"items": {
"$ref": "#/$defs/Door"
}
}
},
"type": "object",
"required": [
"id",
"doorsRequired",
"doors"
]
},
"Door": {
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"type": "object",
"required": [
"id"
]
},
"VehicleCategory": {
"type": "string",
"enum": [
"CAR",
"TRUCK"
]
}
},
"title": "Vehicle",
"$ref": "#/$defs/Vehicle"
}Strict mode produces:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"Vehicle": {
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
},
"description": {
"type": [
"string",
"null"
]
},
"category": {
"oneOf": [
{
"$ref": "#/$defs/VehicleCategory"
},
{
"type": "null"
}
]
},
"doorsOptional": {
"oneOf": [
{
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/$defs/Door"
},
{
"type": "null"
}
]
}
},
{
"type": "null"
}
]
},
"doorsRequired": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/$defs/Door"
},
{
"type": "null"
}
]
}
},
"doors": {
"type": "array",
"items": {
"$ref": "#/$defs/Door"
}
}
},
"type": "object",
"required": [
"id",
"doorsRequired",
"doors"
]
},
"Door": {
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"type": "object",
"required": [
"id"
]
},
"VehicleCategory": {
"type": "string",
"enum": [
"CAR",
"TRUCK"
]
}
},
"title": "Vehicle",
"$ref": "#/$defs/Vehicle"
}Nullability Rules
| GraphQL Type | Strict Mode JSON Schema |
|---|---|
String | {"type": ["string", "null"]} |
String! | {"type": "string"} |
VehicleType (enum) | {"oneOf": [{"$ref": "#/$defs/VehicleType"}, {"type": "null"}]} |
VehicleType! (enum) | {"$ref": "#/$defs/VehicleType"} |
[String] | Array and items both nullable |
[String!] | Array nullable, items non-null |
[String]! | Array non-null, items nullable |
[String!]! | Array and items both non-null |
You can call the help for usage reference:
s2dm export jsonschema --helpProtocol Buffers (Protobuf)
This exporter translates the given GraphQL schema to Protocol Buffers (.proto) format.
Key Features
- Complete GraphQL Type Support: Handles all GraphQL types including scalars, objects, enums, unions, interfaces, and lists
- Selection Query (Required): Use the
--selection-queryflag to specify which types and fields to export via a GraphQL query - Root Type Filtering: Use the
--root-typeflag to export only a specific type and its dependencies - Flatten Naming Mode: Use the
--flatten-namingflag to flatten nested structures into a single message with prefixed field names - Expanded Instance Tags: Use the
--expanded-instancesflag to transform instance tag arrays into nested message structures - Field Nullability: Properly handles nullable vs non-nullable fields from GraphQL schema
- Directive Support: Converts S2DM directives like
@cardinality,@range, and@noDuplicatesto protovalidate constraints - Package Name Support: Use the
--package-nameflag to specify a protobuf package namespace
Example Transformation
Consider the following GraphQL schema and selection query:
GraphQL Schema:
type Cabin {
doors: [Door]
temperature: Float
}
type Door {
isLocked: Boolean
instanceTag: DoorPosition
}
type DoorPosition @instanceTag {
row: RowEnum
side: SideEnum
}
enum RowEnum {
ROW1
ROW2
}
enum SideEnum {
DRIVERSIDE
PASSENGERSIDE
}
type Query {
cabin: Cabin
}Selection Query:
query Selection {
cabin {
doors {
isLocked
instanceTag {
row
side
}
}
temperature
}
}The Protobuf exporter produces:
See Selection Query for more details on the command.
syntax = "proto3";
import "google/protobuf/descriptor.proto";
import "buf/validate/validate.proto";
extend google.protobuf.MessageOptions {
string source = 50001;
}
message RowEnum {
option (source) = "RowEnum";
enum Enum {
ROWENUM_UNSPECIFIED = 0;
ROW1 = 1;
ROW2 = 2;
}
}
message SideEnum {
option (source) = "SideEnum";
enum Enum {
SIDEENUM_UNSPECIFIED = 0;
DRIVERSIDE = 1;
PASSENGERSIDE = 2;
}
}
message DoorPosition {
option (source) = "DoorPosition";
RowEnum.Enum row = 1;
SideEnum.Enum side = 2;
}
message Cabin {
option (source) = "Cabin";
repeated Door doors = 1;
float temperature = 2;
}
message Door {
option (source) = "Door";
bool isLocked = 1;
DoorPosition instanceTag = 2;
}
message Selection {
option (source) = "Query";
optional Cabin cabin = 1;
}The
Querytype from the GraphQL schema is renamed to match the selection query operation name (Selectionin this example).
Selection Query (Required)
The protobuf exporter requires a selection query to determine which types and fields to export:
s2dm export protobuf --schema schema.graphql --selection-query query.graphql --output cabin.protoSee the Selection Query Filtering section for details on how selection queries work.
Root Type Filtering
The --root-type flag can be used to further filter the export. See the Root Type Filtering section for details.
Flatten Naming Mode
Use the --flatten-naming flag to flatten nested object structures into a single message with prefixed field names. This mode works with the selection query to flatten all root-level types selected in the query:
s2dm export protobuf --schema schema.graphql --selection-query query.graphql --output vehicle.proto --flatten-namingYou can optionally combine it with --root-type to flatten only a specific root type:
s2dm export protobuf --schema schema.graphql --selection-query query.graphql --output vehicle.proto --root-type Vehicle --flatten-namingExample transformation:
Given a GraphQL schema and the selection query:
GraphQL Schema:
type Vehicle {
adas: ADAS
}
type ADAS {
abs: ABS
}
type ABS {
isEngaged: Boolean
}
type Query {
vehicle: Vehicle
}Selection Query:
query Selection {
vehicle {
adas {
abs {
isEngaged
}
}
}
}Flatten mode produces:
syntax = "proto3";
import "google/protobuf/descriptor.proto";
import "buf/validate/validate.proto";
extend google.protobuf.MessageOptions {
string source = 50001;
}
message Selection {
bool Vehicle_adas_abs_isEngaged = 1;
}The output message name is derived from the selection query operation name (
Selectionin this example).
Expanded Instance Tags
The --expanded-instances flag transforms instance tag objects into nested message structures instead of repeated fields. This provides compile-time type safety for accessing specific instances.
s2dm export protobuf --schema schema.graphql --selection-query query.graphql --output cabin.proto --expanded-instancesDefault behavior (without flag):
Given a GraphQL schema with instance tags and a selection query:
GraphQL Schema:
type Cabin {
doors: [Door]
}
type Door {
isLocked: Boolean
instanceTag: DoorPosition
}
type DoorPosition @instanceTag {
row: RowEnum
side: SideEnum
}
enum RowEnum {
ROW1
ROW2
}
enum SideEnum {
DRIVERSIDE
PASSENGERSIDE
}
type Query {
cabin: Cabin
}Selection Query:
query Selection {
cabin {
doors {
isLocked
instanceTag {
row
side
}
}
}
}Default output uses repeated fields and includes the instanceTag field:
syntax = "proto3";
import "google/protobuf/descriptor.proto";
import "buf/validate/validate.proto";
extend google.protobuf.MessageOptions {
string source = 50001;
}
message RowEnum {
option (source) = "RowEnum";
enum Enum {
ROWENUM_UNSPECIFIED = 0;
ROW1 = 1;
ROW2 = 2;
}
}
message SideEnum {
option (source) = "SideEnum";
enum Enum {
SIDEENUM_UNSPECIFIED = 0;
DRIVERSIDE = 1;
PASSENGERSIDE = 2;
}
}
message Door {
option (source) = "Door";
optional bool isLocked = 1;
optional DoorPosition instanceTag = 2;
}
message Cabin {
option (source) = "Cabin";
repeated Door doors = 1;
}
message DoorPosition {
option (source) = "DoorPosition";
optional RowEnum.Enum row = 1;
optional SideEnum.Enum side = 2;
}
message Selection {
option (source) = "Query";
optional Cabin cabin = 1;
}With --expanded-instances flag:
The same schema and selection query produce nested messages representing the cartesian product of instance tag values:
syntax = "proto3";
import "google/protobuf/descriptor.proto";
import "buf/validate/validate.proto";
extend google.protobuf.MessageOptions {
string source = 50001;
}
message RowEnum {
option (source) = "RowEnum";
enum Enum {
ROWENUM_UNSPECIFIED = 0;
ROW1 = 1;
ROW2 = 2;
}
}
message SideEnum {
option (source) = "SideEnum";
enum Enum {
SIDEENUM_UNSPECIFIED = 0;
DRIVERSIDE = 1;
PASSENGERSIDE = 2;
}
}
message Door {
option (source) = "Door";
optional bool isLocked = 1;
}
message Cabin {
option (source) = "Cabin";
Door_Row Door = 1 [(buf.validate.field).required = true];
}
message Door_Side {
option (source) = "Door_Side";
optional Door DRIVERSIDE = 1;
optional Door PASSENGERSIDE = 2;
}
message Door_Row {
option (source) = "Door_Row";
Door_Side ROW1 = 1 [(buf.validate.field).required = true];
Door_Side ROW2 = 2 [(buf.validate.field).required = true];
}
message Selection {
option (source) = "Query";
optional Cabin cabin = 1;
}Key differences:
- Instance tag enums (
RowEnum,SideEnum) remain in the output - Types with
@instanceTagdirective (DoorPosition) are excluded from the output - The
instanceTagfield is excluded from the Door message - Intermediate types (
Door_Row,Door_Side) are created as top-level messages - Field names use the type name (
Doornotdoors) - The field becomes required and non-repeated
Directive Support
S2DM directives are converted to protovalidate constraints:
@range(min: 0, max: 100)→[(buf.validate.field).int32 = {gte: 0, lte: 100}]@noDuplicates→[(buf.validate.field).repeated = {unique: true}]@cardinality(min: 1, max: 5)→[(buf.validate.field).repeated = {min_items: 1, max_items: 5}]
GraphQL Schema:
type Vehicle {
speed: Int @range(min: 0, max: 300)
tags: [String] @noDuplicates @cardinality(min: 1, max: 10)
}
type Query {
vehicle: Vehicle
}Selection Query:
query Selection {
vehicle {
speed
tags
}
}Produces:
syntax = "proto3";
import "google/protobuf/descriptor.proto";
import "buf/validate/validate.proto";
extend google.protobuf.MessageOptions {
string source = 50001;
}
message Vehicle {
option (source) = "Vehicle";
int32 speed = 1 [(buf.validate.field).int32 = {gte: 0, lte: 300}];
repeated string tags = 2 [(buf.validate.field).repeated = {unique: true, min_items: 1, max_items: 10}];
}
message Selection {
option (source) = "Query";
optional Vehicle vehicle = 1;
}Type Mappings
GraphQL types are mapped to protobuf types as follows:
| GraphQL Type | Protobuf Type |
|---|---|
String | string |
Int | int32 |
Float | float |
Boolean | bool |
ID | string |
Int8 | int32 |
UInt8 | uint32 |
Int16 | int32 |
UInt16 | uint32 |
UInt32 | uint32 |
Int64 | int64 |
UInt64 | uint64 |
List types are converted to repeated fields:
[String]→repeated string[Int]→repeated int32
Enums are converted to protobuf enums wrapped in a message:
- Each GraphQL enum becomes a protobuf message with the same name
- Inside the message, an
Enumnested enum is created - An
UNSPECIFIEDvalue is added at position 0 - References use the
.Enumsuffix (e.g.,LockStatus.Enum)
Field Nullability:
GraphQL field nullability is preserved in protobuf using the optional keyword and protovalidate constraints:
- Nullable fields (e.g.,
name: String) →optionalproto3 fields - Non-nullable fields (e.g.,
id: ID!) → fields with[(buf.validate.field).required = true]
Example:
type User {
id: ID! # Non-nullable
name: String # Nullable
}Produces:
message User {
option (source) = "User";
string id = 1 [(buf.validate.field).required = true];
optional string name = 2;
}You can call the help for usage reference:
s2dm export protobuf --helpField Number Stability
Important Limitation: Field numbers in generated protobuf files are not stable across schema regenerations when the GraphQL schema changes.
How Field Numbers Are Assigned:
Field numbers are assigned sequentially (starting from 1) based on:
- The iteration order of fields in the GraphQL schema
- Which types/fields are included (affected by
--root-typefiltering) - The flattening logic (when using
--flatten-naming)
Impact on Schema Evolution:
Any change to the GraphQL schema can cause field number reassignments:
# Version 1
type Door {
isLocked: Boolean # becomes field number 1
position: Int # becomes field number 2
}
# Version 2 - Adding a new field
type Door {
id: ID # becomes field number 1
isLocked: Boolean # becomes field number 2 (was 1!)
position: Int # becomes field number 3 (was 2!)
}When Field Number Stability Matters:
Field number changes break compatibility if you have:
- Persistent protobuf data: Data stored in databases, files, or caches will deserialize incorrectly after regeneration
- Rolling deployments: Services using different schema versions cannot communicate during deployment
- Message queues: Messages enqueued before regeneration will fail to deserialize correctly
- Archived data: Historical protobuf-encoded logs or backups become unreadable
Apache Avro
The Avro exporter group provides two commands for exporting GraphQL schemas to Apache Avro formats:
s2dm export avro schema: Exports to Avro schema format (.avsc) using a selection querys2dm export avro protocol: Exports to Avro protocol format (.avdl) for types marked with the@vspec(element: STRUCT)directive
Common Features
Both exporters share the following features:
- Complete GraphQL Type Support: Handles all GraphQL types including scalars, objects, enums, unions, interfaces, and lists
- Type Optimization: Automatically optimizes integer types based on
@rangedirective constraints - Namespace Support: Use the
--namespaceflag to specify an Avro namespace for type references - Expanded Instance Tags: Use the
--expanded-instancesflag to transform instance tag arrays into nested record structures
Avro Schema (s2dm export avro schema)
This exporter translates the given GraphQL schema to Avro schema format.
Selection Query (Required)
The Avro schema exporter requires a selection query to determine which types and fields to export:
s2dm export avro schema --schema schema.graphql --selection-query query.graphql --namespace com.example --output schema.avscSee the Selection Query Filtering section for details on how selection queries work.
Example Transformation
Consider the following GraphQL schema and selection query:
GraphQL Schema:
type Vehicle {
id: ID!
speed: Int
doors: [Door]
}
type Door {
isLocked: Boolean
}
enum Status {
ACTIVE
INACTIVE
}
type Query {
vehicle: Vehicle
}Selection Query:
query VehicleData {
vehicle {
id
speed
doors {
isLocked
}
}
}The Avro schema exporter produces:
s2dm export avro schema --schema schema.graphql --selection-query query.graphql --namespace com.example --output vehicle.avsc{
"type": "record",
"name": "VehicleData",
"namespace": "com.example",
"fields": [
{
"name": "vehicle",
"type": [
"null",
{
"type": "record",
"name": "Vehicle",
"namespace": "com.example",
"fields": [
{
"name": "id",
"type": "string"
},
{
"name": "speed",
"type": ["null", "int"]
},
{
"name": "doors",
"type": [
"null",
{
"type": "array",
"items": [
"null",
{
"type": "record",
"name": "Door",
"namespace": "com.example",
"fields": [
{
"name": "isLocked",
"type": ["null", "boolean"]
}
]
}
]
}
]
}
]
}
]
}
]
}Avro protocol (s2dm export avro protocol)
This exporter generates Avro protocol protocol files (.avdl) for types marked with the @vspec(element: STRUCT) directive. Each type generates a separate protocol file containing the type and its dependencies.
Usage
s2dm export avro protocol --schema schema.graphql --namespace com.example --output ./output-directoryThe command creates one .avdl file per struct type in the output directory.
Example
Given a GraphQL schema:
directive @vspec(element: VspecElement!) on OBJECT
enum VspecElement {
STRUCT
}
enum Status {
ACTIVE
INACTIVE
}
type Vehicle @vspec(element: STRUCT) {
id: ID!
make: String!
status: Status
}
type Person {
name: String
}
type Query {
vehicle: Vehicle
person: Person
}The protocol exporter generates Vehicle.avdl:
@namespace("com.example")
protocol Vehicle {
record Vehicle {
string? id;
string? make;
string? status;
}
}Strict Mode
Use the --strict flag to enforce strict type translation:
s2dm export avro protocol --schema schema.graphql --namespace com.example --output ./output-dir --strictDefault behavior (without --strict):
- Enum types are mapped to
string? - All fields are optional (use
?suffix) - Enum definitions are not included in the protocol
Strict mode (with --strict):
- Enum types are mapped to actual Avro enum types
- Nullability is enforced from GraphQL schema (non-null fields without
?) - Enum definitions are included in the protocol
Example with strict mode:
type Vehicle @vspec(element: STRUCT) {
id: ID!
status: Status
}
enum Status {
ACTIVE
INACTIVE
}Generates:
@namespace("com.example")
protocol Vehicle {
enum Status { ACTIVE, INACTIVE }
record Vehicle {
string id;
Status? status;
}
}Per-Type Namespaces
After the tools collects types mapped with the @vspec directive, it scans the directive annotation for an optional metadata parameter, which you can utilize to specify a custom namespace for individual types using a key-value pair. If not provided, the global namespace from the --namespace flag is used.
directive @vspec(element: VspecElement!, metadata: [KeyValue]) on OBJECT
input KeyValue {
key: String!
value: String!
}
enum VspecElement {
STRUCT
}
type Vehicle @vspec(element: STRUCT, metadata: [{key: "namespace", value: "com.vehicle"}]) {
id: ID!
make: String!
}
type Person @vspec(element: STRUCT) {
name: String!
}Generates:
Vehicle.avdl:
@namespace("com.vehicle")
protocol Vehicle {
record Vehicle {
string? id;
string? make;
}
}Person.avdl:
@namespace("com.example")
protocol Person {
record Person {
string? name;
}
}Help
s2dm export avro protocol --helpType Mappings
Scalar Types
GraphQL scalar types are mapped to Avro types (same for both exporters):
| GraphQL Type | Avro Type |
|---|---|
String | string |
Int | int |
Float | double |
Boolean | boolean |
ID | string |
Int8 | int |
UInt8 | int |
Int16 | int |
UInt16 | int |
UInt32 | long |
Int64 | long |
UInt64 | long |
List Types
Avro Schema (.avsc):
[String]→{"type": "array", "items": ["null", "string"]}[String!]→{"type": "array", "items": "string"}[String]!→{"type": "array", "items": ["null", "string"]}[String!]!→{"type": "array", "items": "string"}
Avro protocol (.avdl):
Non-strict mode (default):
- All list fields →
array<type>?
Strict mode:
[String](nullable list) →array<string>?[String]!(non-null list) →array<string>
Enum Types
Avro Schema (.avsc):
{
"type": "enum",
"name": "Status",
"namespace": "com.example",
"symbols": ["ACTIVE", "INACTIVE"]
}Avro protocol (.avdl):
Non-strict mode (default):
- Enums are mapped to
stringtype
Strict mode:
enum Status {
ACTIVE,
INACTIVE
}Field Nullability
Avro Schema (.avsc):
- Nullable fields (
name: String) →["null", "string"] - Non-nullable fields (
id: ID!) →"string"
Avro protocol (.avdl):
Non-strict mode (default):
- All fields →
string?(always optional)
Strict mode:
- Nullable fields (
name: String) →string? - Non-nullable fields (
id: ID!) →string
Range Directive Optimization
The @range directive automatically optimizes integer type selection between int and long for both exporters:
directive @range(min: Float, max: Float) on FIELD_DEFINITION
directive @vspec(element: VspecElement!) on OBJECT
enum VspecElement {
STRUCT
}
type Sensor @vspec(element: STRUCT) {
temperature: Int64 @range(min: -40, max: 150)
mileage: Int @range(min: 0, max: 5000000000)
}Avro Schema (.avsc) produces:
{
"type": "record",
"name": "Sensor",
"namespace": "com.example",
"fields": [
{
"name": "temperature",
"type": ["null", "int"]
},
{
"name": "mileage",
"type": ["null", "long"]
}
]
}Avro protocol (.avdl) produces:
@namespace("com.example")
protocol Sensor {
record Sensor {
int? temperature;
long? mileage;
}
}Optimization rules:
- If all specified range bounds fit in 32-bit signed range (-2³¹ to 2³¹-1), use
int - If any bound exceeds 32-bit range, use
long - Works with partial ranges:
@range(min: 0)or@range(max: 100) - Without
@range, uses default type mapping
You can call the help for usage reference:
s2dm export avro schema --help
s2dm export avro protocol --helpCommon Features
Selection Query Filtering
All export commands and the compose command support the --selection-query flag to filter the schema based on a GraphQL query. For Protobuf and Avro exporters, the selection query is required.
Usage
s2dm export <format> --selection-query query.graphql ...Or with the compose command:
s2dm compose --selection-query query.graphql ...Behavior
Given a query file query.graphql:
query Selection {
vehicle(instance: "id") {
averageSpeed
adas {
abs {
isEngaged
}
}
}
}The filtered schema will include:
- Only the selected types:
vehicle,adas,abs - Only the selected fields within each type
- Types referenced by field arguments (e.g., enums used in field arguments)
- Only directive definitions that are actually used in the filtered schema
Note: The query must be valid against the schema. Root fields in the query (e.g., vehicle) must exist in the Query type of the schema.
Root Type Filtering
All export commands and the compose command support the --root-type flag to filter the schema to only a specific type and its transitive dependencies.
Usage
s2dm export <format> --root-type Vehicle ...Or with the compose command:
s2dm compose --root-type Vehicle ...Behavior
When you specify a root type:
s2dm compose -s schema.graphql -o composed.graphql -r VehicleThe output will include:
- The
Vehicletype - All types transitively referenced by
Vehicle - Enums used in fields of these types
- Scalar types used in fields
Types not connected to
Vehiclewill be filtered out.
Combining with Selection Query:
When used with --selection-query, root type filtering is applied after the selection query filtering, further narrowing the results to only types reachable from the specified root type.
Naming Configuration
The naming configuration defines naming conventions for GraphQL schema elements using a YAML file. This configuration can be used for:
- Transformation: Converting element names to match desired conventions
- Validation: Checking that element names follow specified conventions
Commands that support --naming-config:
check constraints- naming validationcompose- naming transformationexport jsonschema- naming transformationexport protobuf- naming transformationexport shacl- naming transformationexport vspec- naming transformation
Configuration Format
The naming configuration is defined in a YAML file with the following structure:
type:
object: PascalCase
interface: PascalCase
input: PascalCase
enum: PascalCase
union: PascalCase
scalar: PascalCase
field:
object: camelCase
interface: camelCase
input: snake_case
enumValue: MACROCASE
instanceTag: COBOL-CASE
argument:
field: camelCaseSupported Case Formats
The naming configuration supports the following case conversion formats:
- camelCase:
somePropertyName - PascalCase:
SomePropertyName - snake_case:
some_property_name - kebab-case:
some-property-name - MACROCASE:
SOME_PROPERTY_NAME - COBOL-CASE:
SOME-PROPERTY-NAME - flatcase:
somepropertyname - TitleCase:
Some Property Name
Example
Given this GraphQL schema:
type vehicle_info {
avg_speed: Float
fuel_type: fuel_type_enum
}
enum fuel_type_enum {
GASOLINE_TYPE
DIESEL_TYPE
}And this naming configuration:
type:
object: PascalCase
enum: PascalCase
field:
object: camelCase
enumValue: PascalCase
instanceTag: PascalCase # Required for transformation if `enumValue` is defined.For transformation, names are converted to match the configuration:
- Type:
vehicle_info→VehicleInfo - Field:
avg_speed→avgSpeed - Field:
fuel_type→fuelType - Enum type:
fuel_type_enum→FuelTypeEnum - Enum values:
GASOLINE_TYPE→GasolineType,DIESEL_TYPE→DieselType
For validation, the schema is checked against the configuration:
vehicle_infofails (expected PascalCase)avg_speedfails (expected camelCase)fuel_typefails (expected camelCase)fuel_type_enumfails (expected PascalCase)GASOLINE_TYPEfails (expected PascalCase)
Validation Rules
The naming configuration system enforces several validation rules to ensure consistency and correctness:
Element Type Validation:
- Valid element types: Only
type,field,argument,enumValue, andinstanceTagare allowed - Context restrictions: Some element types cannot have context-specific configurations:
enumValueandinstanceTagare contextless and use a single case formatargumentcan only havefieldcontext
- Value type validation: Element values must be either strings (case formats) or dictionaries (for context-specific configurations)
Context Validation:
- Type contexts:
object,interface,input,scalar,union,enum - Field contexts:
object,interface,input - Argument contexts:
field
Case Format Validation:
- Valid case formats:
camelCase,PascalCase,snake_case,kebab-case,MACROCASE,COBOL-CASE,flatcase,TitleCase - Format enforcement: Only recognized case formats are accepted; invalid formats will cause validation errors
Context-specific Rules:
- EnumValue-InstanceTag pairing: If
enumValueis present in the configuration for transformation commands,instanceTagmust also be present. - InstanceTag preservation: The literal field name
instanceTagis never transformed or validated, regardless of naming configuration, to preserve its semantic meaning.
Notes
- Built-in GraphQL types (
String,Int,Float,Boolean,ID,Query,Mutation,Subscription) are never transformed. - When an element type is not configured, it is neither transformed or nor validated.
Expanded Instances
All export commands and the compose command support the --expanded-instances flag that transforms instance tag arrays into nested structures.
Usage
s2dm export <format> --expanded-instances ...Or with the compose command:
s2dm compose --expanded-instances ...Transformation Behavior
Given a schema with instance tags:
type Cabin {
doors: [Door]
}
type Door {
isLocked: Boolean
instanceTag: DoorPosition
}
type DoorPosition @instanceTag {
row: RowEnum
side: SideEnum
}
enum RowEnum {
ROW1
ROW2
}
enum SideEnum {
DRIVERSIDE
PASSENGERSIDE
}Without --expanded-instances (default):
The schema structure remains as-is with list fields and instanceTag preserved.
With --expanded-instances:
The schema is transformed to use nested intermediate types:
type Cabin {
Door: Door_Row
}
type Door_Row {
ROW1: Door_Side
ROW2: Door_Side
}
type Door_Side {
DRIVERSIDE: Door
PASSENGERSIDE: Door
}
type Door {
isLocked: Boolean
}
enum RowEnum {
ROW1
ROW2
}
enum SideEnum {
DRIVERSIDE
PASSENGERSIDE
}Key Changes
- Field names: Plural list fields (
doors) are renamed to singular (Door) - Field types: List types (
[Door]) become intermediate types (Door_Row) - Intermediate types: New types are created representing the cartesian product of instance tag enums (
Door_Row,Door_Side) - Instance tag removal: The
instanceTagfield is removed from the base type (Door) - Type removal: Types with
@instanceTagdirective (DoorPosition) are removed from the schema - Enum preservation: Instance tag enums (
RowEnum,SideEnum) remain in the schema