Candid interfaces as data, not text
candid-core parses a .did file once, through the official Candid type
checker, and projects the checked result into a validated type graph with a SHA-256
identity over it. Rust and TypeScript packages read that one graph to give you typed
schemas, validation, and a binary codec.
type Status = variant { ok; busy : nat32; failed : text };
type Numbered = variant { 0 : null; 5 : nat };
type Tree = variant { leaf : nat; node : record { left : Tree; right : Tree } };{
"format": "candid-core",
"format_version": 1,
"semantics_profile": "candid-1",
"canonicalization_profile": "candid-core-canon-1",
"identities": {
"contract": "candid-core:contract:v1:sha256:68a6425dfc8619f43794ebbbf27ea4979c55569cb86988efec0f6c06f0a2c1f9"
},
"declarations": [{ "name": "EmptyOpt", "type": 0 }],
"types": [
{ "kind": "opt", "inner": 1 },
{ "kind": "primitive", "primitive": "empty" }
]
}// Generated by candid-core-ts from a candid-core Contract. Do not edit.
import * as $ from "@candid-core/schema";
type $Numbered = { tag: "_0_" } | { tag: "_5_"; value: bigint };
const $Numbered: $.Schema<$Numbered> = $.c.rec(() => $.c.variant({ _0_: $.c.null, _5_: $.c.nat }));
export { $Numbered as Numbered };
type $Status = { tag: "ok" } | { tag: "busy"; value: number } | { tag: "failed"; value: string };
const $Status: $.Schema<$Status> = $.c.rec(() => $.c.variant({ ok: $.c.null, busy: $.c.nat32, failed: $.c.text }));
export { $Status as Status };
type $Tree = { tag: "leaf"; value: bigint } | { tag: "node"; value: { left: $Tree; right: $Tree } };
const $Tree: $.Schema<$Tree> = $.c.rec(() => $.c.variant({ leaf: $.c.nat, node: $.c.record({ left: $Tree, right: $Tree }) }));
export { $Tree as Tree };import { validate } from "@candid-core/schema/validate";
import { encode } from "@candid-core/schema/codec";
import { Status } from "./generated/interface";
// `Status` is one object that is both a TypeScript type and a runtime schema.
const value: Status = { tag: "busy", value: 3 };
validate(Status, value); // { ok: true } — never throws
encode(Status, value); // { ok: true, bytes } or { ok: false, issues } — never throws
Three of the four panels above are files checked into the repository, compared byte for byte
by a test. The generated module is additionally compiled by the pinned TypeScript under
strict, so the mapping between the alias and the schema builder is checked by
the compiler rather than asserted in prose.
A .did file is text, and text is where the trouble starts
Two files can describe exactly the same callable interface while differing in type names, field order, comments and whitespace. Diffing them tells you nothing reliable.
// Version A
type Account = record {
owner : principal;
balance : nat;
};
service : { get : (Account) -> (nat) query }type Holder = record { balance : nat; owner : principal };
service : {
get : (Holder) -> (nat) query;
}
Same wire interface. Different bytes, different type name, different field order, different
comments. Compile both and their interface_id is the same string, because that
identity covers the canonical type graph reachable from the actor and nothing else. Rename a
method or change an argument type and it moves.
How the identities differ →
The second problem is duplication. Every tool that consumes a .did file parses it
again and re-implements Candid's rules: alias resolution, the 32-bit field-label hash,
recursive types, function modes, service-class constructors. Those re-implementations disagree
in small ways, and the disagreements surface as wrong encodings rather than compile errors.
Most of them also parse whatever you hand them without bound, which stops being academic the
moment the source comes from a user, an agent, or a remote registry.
Parse once, project into a model everything else reads
The parsing and type checking are delegated to the official candid_parser. What
candid-core owns is the projection: a validated graph, one canonical byte encoding, and the
identities computed over it.
What you can build on it
Each of these is a thing the model makes straightforward, with the package that provides it.
Compare one string. Renamed types, reordered fields, reformatting and comments leave
interface_id where it was; adding a method or changing an argument type moves it.
opt nat becomes bigint | null. A variant becomes
{ tag, value }. Each declaration also ships a schema the compiler proves matches
its alias in both directions.
validate never throws, on any value. Issues carry a stable code and a
$-rooted path, and the walk is bounded by depth, element and issue budgets.
A schema-directed implementation of the wire format with no runtime dependencies. It refuses ambiguous input rather than guessing, and returns issues instead of throwing.
@candid-core/schema/contract Handle an interface you learn at runtimeschemaFromContract builds the same schema objects from a Contract document,
with no code generation step — for an explorer, a wallet, or an agent tool.
.did source you did not write
Over-limit input comes back as a resource_limit_exceeded diagnostic carrying
{ resource, limit, observed } rather than exhausting memory, CPU or the stack.
Every value carries its own Candid tag and its exact bits, so a large nat, a
negative zero, and the difference between opt null and an absent field all survive.
One model, five units
Everything in the repository reads the same Contract graph. Nothing downstream can change it.
The Rust library and the candid-core binary. Four feature surfaces, from a
pure model with no Candid engine up to a sandboxed filesystem compiler.
The Rust TypeScript generator. Unpublishable on purpose — a crates.io name is permanent and this one is not decided yet.
npm · 0.2.0 @candid-core/schemaThe schema runtime: builders and inference, validation, the binary codec, and runtime schemas from a Contract document.
npm · 0.1.0 @candid-core/cliThe compiler and generator as WebAssembly, for a JavaScript project with no Rust toolchain.
internal candid-core-fuzzIts own workspace root and lockfile, with targets mirroring the feature surface. Part of the evidence, not part of the dependency graph.
Reference The full inventoryEvery crate and package with its manifest path, publication status, version, features and subpath exports — plus a table for choosing between them.
Start where you are
Install @candid-core/schema, describe an interface, validate a value, encode
it. Then stop hand-writing schemas.
.did to a Contract
One dependency line, one call, and you have a validated graph with identities you can compare and cache on.
Command line No code at allCompile a .did file to a canonical Contract document or an envelope, and
re-validate one you were given.
The verification battery, the cross-implementation evidence, and an honest ledger of what is still pending.
The TypeScript on this site is the surface the repository builds, published as
@candid-core/schema 0.3.0-beta.1 and @candid-core/cli 0.2.0-beta.1 under the npm beta dist-tag. latest, 0.2.0 and
0.1.0, still has the old one, and Migrating from 0.2.0 lists every difference with before and
after code.
The Rust crate is 0.1.0-beta.3 and the published @candid-core/schema
is 0.2.0. Any release before 1.0 may change the public API, the serialized
shapes, the canonical bytes, and therefore every identity computed over them, so pin an
exact version. Because the crate version is a prerelease, a caret requirement selects
nothing — the dependency line is candid-core = "=0.1.0-beta.3".
Versions, changes and release policy →