skir
v0.0.5
Published
[](https://www.npmjs.com/package/skir) [](https://github.com/gepheum/skir/actions)
Readme
Skir
Like Protocol Buffer, but better.
Skir is a language for representing data types, constants and RPC interfaces. Skir files have the .skir extension.
// shapes.skir
struct Point {
x: int32;
y: int32;
label: string;
}
struct Polyline {
points: [Point];
label: string;
}
const TOP_RIGHT_CORNER: Point = {
x = 600,
y = 400,
label = "top-right corner",
};
// Method of an RPC interface
method IsPalindrome(string): bool;The Skir compiler takes in a set of .skir files and generates source files containg the definition of the data types and constants in various programming languages. It also generates functions for serializing the data types to either JSON or a more compact binary format.
from skirout import shapes # source file generated by the Skir compiler
point = shapes.Point(x=3, y=4, label="P")
json = shapes.Point.SERIALIZER.to_json(point)
point = shapes.Point.SERIALIZER.from_json(json)
assert(point == shapes.Point(x=3, y=4, label="P"))As of November 2025, Skir has official plugins for:
- JavaScript/TypeScript: plugin, example
- Python: plugin, example
- C++: plugin, example
- Java: plugin, example
- Kotlin: plugin, example
- Dart: plugin, example
Other official and unofficial plugins will come.
Why Skir
- Generates code in different languages. This makes Skir ideal for systems where different services are written in different languages but need to exchange structured data.
- Supports serialization to JSON or binary format.
- Serialize now, deserialize in 100 years. Skir is designed with backward and forward compatibility in mind. You can evolve your data schemas by adding new fields or renaming fields. You will still be able to deserialize old values, and you won't break existing applications that use older versions of the schema.
- Generates code that makes no compromise with type safety.
- Allows you to define typesafe interfaces between your services or your backend and your frontend.
- No boilerplate code when you adding a new method to a service.
Language reference
Records
There are two types of records: structs and enums.
Structs
Use the keyword struct to define a struct, which is a collection of fields of different types.
The fields of a struct have a name, but during serialization they are actually identified by a number, which can either be set explicitly:
struct Point {
x: int = 0;
y: int = 1;
label: string = 2;
}or implicitly:
struct Point {
x: int; // implicitly set to 0
y: int; // implicitly set to 1
label: string; // implicitly set to 2
}If you're not explicitly specifying the field numbers, you must be careful not to change the order of the fields or else you won't be able to deserialize old values.
// BAD: you can't reorder the fields and keep implicit numbering
struct Point {
label: string;
x: int;
y: int;
}
// GOOD
struct Point {
label: string = 2;
// Fine to rename fields
x_coordinate: int = 0;
y_coordinate: int = 1;
// Fine to add new fields
color: Color = 3;
}Enums
Enums in Skir are similar to enums in Rust. An enum value is one of several possible variants, and each variant can optionally have data associated with it.
// Indicates whether an operation succeeded or failed.
enum OperationStatus {
SUCCESS; // a constant field
error: string; // a wrapper field
}In this example, an OperationStatus is one of these 3 things:
- the
SUCCESSconstant - an
errorwith a string value UNKNOWN: a special implicit variant common to all enums
If you need a variant to hold multiple values, wrap them inside a struct:
struct MoveAction {
x: int32;
y: int32;
}
enum BoardGameTurn {
PASS;
move: MoveAction;
}Like structs, the fields of an enum have a number, and the numbering can be explicit or implicit.
enum ExplicitNumbering {
// The numbers don't need to be consecutive.
FOO = 10;
bar: string = 2;
}
enum ImplicitNumbering {
// Implicit numbering is 1-based.
// 0 is reserved for the special UNKNOWN variant.
FOO; // = 1
bar: string; // = 2
}The fields numbers are used for identifying the variants in the serialization format (not the field names). You must be careful not to change the number of a field, or you won't be able to deserialize old values. For example, if you're using implicit numbering, you must not reorder the fields.
It is always fine to rename an enum, rename the fields of an enum, or add new fields to an enum.
Nesting records
You can define a record (struct or enum) within the definition of another record. This is simply for namespacing, and it can help make your .skir files more organized.
enum Status {
OK;
struct Error {
message: string;
}
error: Error;
}
struct Foo {
// Note the dot notation to refer to the nested record.
error: Status.Error;
}Removed fields
When removing a field from a struct or an enum, you must mark the removed number in the record definition using the removed keyword. The syntax is different whether you're using explicit or implicit numbering:
struct ExplicitNumbering {
a: string = 0;
b: string = 1:
d: string = 3;
removed 2, 4;
}
struct ImplicitNumbering {
a: string;
b: string:
removed;
d: string;
removed;
}Data types
Primitive types
bool: true or falseint32: a signed 32-bits integerint64: a signed 64-bits integeruint64: an unsigned 64-bits integerfloat32: a 32-bits floating point numberfloat64: a 64-bits floating point numberstring: a Unicode stringbytes: a sequence of bytestimestamp: a specific instant in time represented as an integral number of milliseconds since the Unix epoch, from 100M days before the Unix epoch to 100M days after the Unix epoch
Array type
Wrap the item type inside square brackets to represent an array of items, e.g. [string] or [User].
Keyed arrays
If the items are structs and one of the struct fields can be used to identify every item in the array, you can add the field name next to a pipe character: [Item|key_field].
Example:
struct User {
id: uint64;
name: string;
}
struct UserRegistry {
users: [User|id];
}Language plugins will generate methods allowing you to perform key lookups in the array using a hash table. For example, in Python:
user = user_registry.users.find(user_id)
if user:
do_something(user)If the item key is nested within another struct, you can chain the field names like so: [Item|a.b.c].
The key type must be a primitive type of an enum type. If it's an enum type, add .kind at the end of the key chain:
enum Weekday {
MONDAY;
TUESDAY;
WEDNESDAY;
THURSDAY;
FRIDAY;
SATURDAY;
SUNDAY;
}
struct WeekdayWorkStatus {
weekday: Weekday;
working: bool;
}
struct Employee {
weekly_schedule: [WeekdayWorkStatus|weekday.kind];
}Optional type
Add a question mark at the end of a non-optional type to make it optional. An other_type? value is either an other_type or null.
Constants
You can define constants of any type with the const keyword. The syntax for representing the value is similar to JSON, with the following differences:
- object keys must not be quoted
- trailing commas are allowed and even encouraged
- strings can be single-quoted or double-quoted
- strings can span multiple lines by escaping new line characters
const PI: float64 = 3.14159;
const LARGE_CIRCLE: Circle = {
center: {
x: 100,
y: 100,
},
radius: 100,
color: {
r: 255,
g: 0,
b: 255,
label: "fuschia",
},
};
const MULTILINE_STRING: string = 'Hello\
world\
!';
const SUPPORTED_LOCALES: [string] = [
"en-GB",
"en-US",
"es-MX",
];
// Use strings for enum constants.
const REST_DAY: Weekday = "SUNDAY";
// Use { kind: ..., value: ... } for enum variants holding a value.
const NOT_IMPLEMENTED_ERROR: OperationStatus = {
kind: "error",
value: "Not implemented",
};Methods
The method keyword allows you to define the signature of a remote method.
struct GetUserProfileRequest {
// ...
}
struct GetUserProfileResponse {
// ...
}
method GetUserProfile(GetUserProfileRequest): GetUserProfileResponse;The request and response can have any type.
Imports
The import statement allows you to import types from another module. You can either specify the names to import, or import the whole module with an alias using the as keyword.
import Point, Circle from "geometry/geometry.skir";
import * as color from "color.skir";
struct Rectangle {
top_left: Point;
bottom_right: Point;
}
struct Disk {
circle: Circle;
fill_color: color.Color; // the type is defined in the "color.skir" module
}The path is always relative to the root of the Skir source directory.
Serialization formats
When serializing a data structure, you can chose one of 3 formats.
JSON, dense flavor
This is the serialization format you should chose in most cases.
Structs are serialized as JSON arrays, where the field numbers in the index definition match the indexes in the array. Enum constants are serialized as numbers.
struct User {
user_id: int;
removed;
name: string;
rest_day: Weekday;
pets: [Pet];
nickname: string;
}
const JOHN_DOE = {
user_id: 400,
name: "John Doe",
rest_day: "SUNDAY",
pets: [
{ name: "Fluffy" },
{ name: "Fido" },
],
nickname: "",
}The dense JSON representation of JOHN_DOE is:
[400,0,"John Doe",7,[["Fluffy"],["Fido"]]]A couple observations:
- Removed fields are replaced with zeros
- Trailing fields with default values (
nicknamein this example) are omitted
This format is not very readable, but it's compact and it allows you to rename fields in your struct definition without breaking backward compatibility.
JSON, readable flavor
Structs are serialized as JSON objects, and enum constants are serialized as strings.
The readable JSON representation of JOHN_DOE is:
{
"user_id": 400,
"name": "Johm Doe",
"rest_day": "SUNDAY",
"pets": [
{ "name": "Fluffy" },
{ "name": "Fido" }
]
}This format is more verbose and readable, but it should not be used if you need persistence, because Skir allows fields to be renamed in record definitions. In other words, never store a readable JSON on disk or in a database.
Binary format
This format is a bit more compact than JSON, and serialization/deserialization can be faster in languages like C++. Only prefer this format over JSON when the small performance gain is likely to matter, which should be rare.
Skir services
Calling a method with cURL
curl -X POST \
-H "Content-Type: application/json" \
-d '{"method": "MethodName", "request": {"foo": 3, "bar": []}}' \
http://localhost:8787/myapi