candid-core 0.1.0-beta.3 · pre-1.0

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.

candidcrates/candid-core-ts/tests/fixtures/variants.did
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 } };

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.

candida.did
// Version A
type Account = record {
  owner : principal;
  balance : nat;
};
service : { get : (Account) -> (nat) query }
candidb.did
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.

.did source text, imports candid_parser parse + type check DELEGATED, PINNED Contract arena of type nodes validated, canonical contract_id interface_id Rust API + CLI validate, identify TypeScript generator aliases + schemas Schema runtime validate, codec, actor Every step above runs under a Limits policy and a per-operation budget, and fails with a structured diagnostic rather than a hang.

What you can build on it

Each of these is a thing the model makes straightforward, with the package that provides it.

candid-core · CLI Tell whether an interface actually changed

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.

candid-core-ts Get the TypeScript you would have written

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.

@candid-core/schema/validate Validate a value before it reaches your UI

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.

@candid-core/schema/codec Encode and decode Candid without an agent library

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 runtime

schemaFromContract builds the same schema objects from a Contract document, with no code generation step — for an explorer, a wallet, or an agent tool.

candid-core Accept .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.

candid-core · host-value Move Candid values through JSON losslessly

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.

Start where you are

Published as a beta

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.

Pre-1.0, and it says so

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 →