
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
[](https://www.npmjs.com/package/soiac) [](https://github.com/gepheum/soiac/actions)
Like Protocol Buffer, but better.
Soia is a language for representing data types, constants and RPC interfaces. Soia files have the .soia extension.
// shapes.soia
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 soia compiler takes in a set of soia 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 soiagen import shapes # source file generated by the soia 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, soia has official plugins for:
Other official and unofficial plugins will come.
There are two types of records: structs and enums.
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 in Soia 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:
SUCCESS constanterror with a string valueUNKNOWN: a special implicit variant common to all enumsIf 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.
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 soia 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;
}
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;
}
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 epochWrap the item type inside square brackets to represent an array of items, e.g. [string] or [User].
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];
}
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.
You can define constants of any soia type with the const keyword. The syntax for representing the value is similar to JSON, with the following differences:
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",
};
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 soia type.
The import statement allows you to import types from another soia 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.soia";
import * as color from "color.soia";
struct Rectangle {
top_left: Point;
bottom_right: Point;
}
struct Disk {
circle: Circle;
fill_color: color.Color; // the type is defined in the "color.soia" module
}
The path is always relative to the root of the soia source directory.
When serializing a soia data structure, you can chose one of 3 formats.
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:
nickname in this example) are omittedThis format is not very readable, but it's compact and it allows you to rename fields in your struct definition without breaking backward compatibility.
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 soia allows fields to be renamed in record definitions. In other words, never store a readable JSON on disk or in a database.
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.
curl -X POST \
-H "Content-Type: application/json" \
-d '{"method": "MethodName", "request": {"foo": 3, "bar": []}}' \
http://localhost:8787/myapi
FAQs
[](https://www.npmjs.com/package/soiac) [](https://github.com/gepheum/soiac/actions)
The npm package soiac receives a total of 22 weekly downloads. As such, soiac popularity was classified as not popular.
We found that soiac demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.