Dart
This guide explains how to use Skir in a Dart project. Targets Dart 3.0 and higher.
Set up
In your skir.yml file, add the following snippet under generators:
- mod: skir-dart-gen
outDir: ./src/skirout
config: {}The generated Dart code has a runtime dependency on the skir_client library. Add this line to your pubspec.yaml file under dependencies:
skir_client: ^1.0.2 # Use the latest versionFor more information, see this Dart project example.
Generated code guide
The examples below are for the code generated from this .skir file.
Referring to generated symbols
// Import the given symbols from the Dart module generated from "user.skir"
import 'package:skir_dart_example/skirout/user.dart';
// Now you can use: tarzan, User, UserHistory, UserRegistry, etc.Struct classes
For every struct S in the .skir file, skir generates a frozen (deeply immutable) class S and a mutable class S_mutable.
Frozen struct classes
// To construct a frozen User, call the User constructor.
final john = User(
// All fields are required.
userId: 42,
name: "John Doe",
quote: "Coffee is just a socially acceptable form of rage.",
pets: [
User_Pet(
name: "Dumbo",
heightInMeters: 1.0,
picture: "🐘",
),
],
subscriptionStatus: SubscriptionStatus.free,
// foo: "bar",
// ^ Does not compile: 'foo' is not a field of User
);
assert(john.name == "John Doe");
// john.name = "John Smith";
// ^ Does not compile: all the properties are read-only
// You can also construct a frozen User using the builder pattern with a
// mutable instance as the builder.
final User jane = (User.mutable()
..userId = 43
..name = "Jane Doe"
..pets = [
User_Pet(name: "Fluffy", heightInMeters: 0.2, picture: "🐱"),
User_Pet.mutable()
..name = "Fido"
..heightInMeters = 0.25
..picture = "🐶"
..toFrozen(),
])
.toFrozen();
// Fields not explicitly set are initialized to their default values.
assert(jane.quote == "");
// User.defaultInstance is an instance of User with all fields set to their
// default values.
assert(User.defaultInstance.name == "");
assert(User.defaultInstance.pets.isEmpty);Mutable struct classes
// 'User_mutable' is a dataclass similar to User except it is mutable.
// Use User.mutable() to create a new instance.
final User_mutable mutableLyla = User.mutable()..userId = 44;
mutableLyla.name = "Lyla Doe";
final UserHistory_mutable userHistory = UserHistory.mutable();
userHistory.user = mutableLyla;
// ^ The right-hand side of the assignment can be either frozen or mutable.
// The 'mutableUser' getter provides access to a mutable version of 'user'.
// If 'user' is already mutable, it returns it directly.
// If 'user' is frozen, it creates a mutable shallow copy, assigns it to
// 'user', and returns it.
// The user is currently 'mutableLyla', which is mutable.
assert(identical(userHistory.mutableUser, mutableLyla));
// Now assign a frozen User to 'user'.
userHistory.user = john;
// Since 'john' is frozen, mutableUser makes a mutable shallow copy of it.
assert(!identical(userHistory.mutableUser, john));
userHistory.mutableUser.name = "John the Second";
assert(userHistory.user.name == "John the Second");
assert(userHistory.user.userId == 42);
// Similarly, 'mutablePets' provides access to a mutable version of 'pets'.
// It returns the existing list if already mutable, or creates and returns a
// mutable shallow copy.
mutableLyla.mutablePets.add(User_Pet(
name: "Simba",
heightInMeters: 0.4,
picture: "🦁",
));
mutableLyla.mutablePets.add(User_Pet.mutable()..name = "Cupcake");Converting between frozen and mutable
// toMutable() does a shallow copy of the frozen struct, so it's cheap. All the
// properties of the copy hold a frozen value.
final evilJane = (jane.toMutable()
..name = "Evil Jane"
..quote = "I solemnly swear I am up to no good.")
.toFrozen();
assert(evilJane.name == "Evil Jane");
assert(evilJane.userId == 43);Writing logic agnostic of mutability
// 'User_orMutable' is a type alias for the sealed class that both 'User' and
// 'User_mutable' implement.
void greet(User_orMutable user) {
print("Hello, ${user.name}");
}
greet(jane);
// Hello, Jane Doe
greet(mutableLyla);
// Hello, Lyla DoeEnum classes
The definition of the SubscriptionStatus enum in the .skir file is:
enum SubscriptionStatus {
FREE;
trial: Trial;
PREMIUM;
}Making enum values
final johnStatus = SubscriptionStatus.free;
final janeStatus = SubscriptionStatus.premium;
final jolyStatus = SubscriptionStatus.unknown;
// Use wrapX() or createX() for wrapper fields:
// - wrapX() expects the value to wrap.
// - createX() creates a new struct with the given params and wraps it
final roniStatus = SubscriptionStatus.wrapTrial(
SubscriptionStatus_Trial(
startTime: DateTime.fromMillisecondsSinceEpoch(1234, isUtc: true)),
);
// More concisely, with createX():
final ericStatus = SubscriptionStatus.createTrial(
startTime: DateTime.fromMillisecondsSinceEpoch(5678, isUtc: true),
);Conditions on enums
assert(johnStatus == SubscriptionStatus.free);
assert(janeStatus == SubscriptionStatus.premium);
assert(jolyStatus == SubscriptionStatus.unknown);
if (roniStatus is SubscriptionStatus_trialWrapper) {
assert(roniStatus.value.startTime.millisecondsSinceEpoch == 1234);
} else {
throw AssertionError();
}
String getSubscriptionInfoText(SubscriptionStatus status) {
// Use pattern matching for typesafe switches on enums.
return switch (status) {
SubscriptionStatus_unknown() => "Unknown subscription status",
SubscriptionStatus.free => "Free user",
SubscriptionStatus.premium => "Premium user",
SubscriptionStatus_trialWrapper(:final value) =>
"On trial since ${value.startTime}",
};
}Serialization
Every frozen struct class and enum class has a static readonly serializer property which can be used for serializing and deserializing instances of the class.
final serializer = User.serializer;
// Serialize 'john' to dense JSON.
final String johnDenseJson = serializer.toJsonCode(john);
print(johnDenseJson);
// [42,"John Doe",...]
// Serialize 'john' to readable JSON.
print(serializer.toJsonCode(john, readableFlavor: true));
// {
// "user_id": 42,
// "name": "John Doe",
// "quote": "Coffee is just a socially acceptable form of rage.",
// "pets": [
// {
// "name": "Dumbo",
// "height_in_meters": 1.0,
// "picture": "🐘"
// }
// ],
// "subscription_status": "FREE"
// }
// The dense JSON flavor is the flavor you should pick if you intend to
// deserialize the value in the future. Skir allows fields to be renamed, and
// because field names are not part of the dense JSON, renaming a field does
// not prevent you from deserializing the value.
// You should pick the readable flavor mostly for debugging purposes.
// Serialize 'john' to binary format.
final Uint8List johnBytes = serializer.toBytes(john);
// The binary format is not human readable, but it is slightly more compact
// than JSON, and serialization/deserialization can be a bit faster in
// languages like C++. Only use it when this small performance gain is likely
// to matter, which should be rare.Deserialization
// Use fromJson(), fromJsonCode() and fromBytes() to deserialize.
final reserializedJohn = serializer.fromJsonCode(johnDenseJson);
assert(reserializedJohn.name == "John Doe");
final reserializedJane = serializer.fromJsonCode(
serializer.toJsonCode(jane, readableFlavor: true),
);
assert(reserializedJane.name == "Jane Doe");
assert(serializer.fromBytes(johnBytes) == john);Frozen lists and copies
final pets = [
User_Pet(name: "Fluffy", heightInMeters: 0.25, picture: "🐶"),
User_Pet(name: "Fido", heightInMeters: 0.5, picture: "🐻"),
];
final jade = User(
userId: 46,
name: "Jade",
quote: "",
pets: pets,
// ^ makes a copy of 'pets' because 'pets' is mutable
subscriptionStatus: SubscriptionStatus.unknown,
);
// jade.pets.add(...)
// ^ Compile-time error: pets is a frozen list
assert(!identical(jade.pets, pets));
final jack = User(
userId: 47,
name: "Jack",
quote: "",
pets: jade.pets,
// ^ doesn't make a copy because 'jade.pets' is frozen
subscriptionStatus: SubscriptionStatus.unknown,
);
assert(identical(jack.pets, jade.pets));Keyed lists
final userRegistry = UserRegistry(
users: [john, jane, mutableLyla, evilJane],
);
// find() returns the user with the given key (specified in the .skir file).
// In this example, the key is the user id.
// The first lookup runs in O(N) time, and the following lookups run in O(1)
// time.
assert(userRegistry.users.findByKey(42) == john);
assert(userRegistry.users.findByKey(100) == null);
// If multiple elements have the same key, find() returns the last one.
// Duplicates are allowed but generally discouraged.
assert(userRegistry.users.findByKey(43) == evilJane);Constants
print(tarzan);
// User(
// userId: 123,
// name: "Tarzan",
// quote: "AAAAaAaAaAyAAAAaAaAaAyAAAAaAaAaA",
// pets: [
// User_Pet(
// name: "Cheeta",
// heightInMeters: 1.67,
// picture: "🐒",
// ),
// ],
// subscriptionStatus: SubscriptionStatus.wrapTrial(
// SubscriptionStatus_Trial(
// startTime: DateTime.fromMillisecondsSinceEpoch(
// // 2025-04-02T11:13:29.000Z
// 1743592409000
// ),
// )
// ),
// )Skir services
Starting a skir service on an HTTP server - full example here.
Sending RPCs to a skir service - full example here.
Reflection
Reflection allows you to inspect a skir type at runtime.
import 'package:skir/skir.dart' as skir;
final fieldNames = <String>[];
for (final field in User.serializer.typeDescriptor.fields) {
fieldNames.add(field.name);
}
print(fieldNames);
// [user_id, name, quote, pets, subscription_status]
// A type descriptor can be serialized to JSON and deserialized later.
final typeDescriptor = skir.TypeDescriptor.parseFromJson(
User.serializer.typeDescriptor.asJson,
);
print("Type descriptor deserialized successfully");