Interoperability
Twilic interoperability requires both sides to agree on version and profile. The current specification family is v3. Rust, Go, JavaScript, and Zig are on the v3 interop line; other published SDKs remain on v2 until their v3 support lands.
The interoperability contract
Rust encoder → [bytes] → Go decoder ✓ (v3)
JS encoder → [bytes] → Zig decoder ✓ (v3)
Python batch → [bytes] → Java decode ✓ (v2)
JS stateful → [bytes] → Rust session ✓ (with matching session state and wire line)Requirements:
- Both sides implement the same Twilic version/profile (
v3among Rust/Go/JS/Zig,v2among the remaining SDKs, or an explicitly negotiated mix) - Same profile used (Dynamic decodes Dynamic; session patches need session decoder)
- Field maps use string keys for cross-language stability
Cross-language smoke test
Rust producer → Go consumer
// producer.rs
use twilic::{encode, Value};
let value = Value::Map(vec![
("event".into(), Value::String("user.signup".into())),
("user_id".into(), Value::U64(9001)),
]);
let bytes = encode(&value)?;
std::fs::write("event.twl", &bytes)?;// consumer.go
data, _ := os.ReadFile("event.twl")
value, err := twilic.Decode(data)Works across all 18 official SDKs without a shared schema file in Dynamic mode.
Shared test fixtures
The Twilic monorepo maintains cross-language conformance fixtures. Run interop checks per SDK:
# Python
cd twilic-python && ./scripts/check-interop.sh
# Rust (reference)
cd twilic-rust && cargo test
# JavaScript
cd twilic-js && pnpm testProfile-specific interop
| Profile | Interop notes |
|---|---|
| Dynamic | Full cross-language; no shared state |
| Batch | Decoder must expect batch message kind |
| Bound | Encoder and decoder must share identical schema |
| Stateful | Decoder must maintain matching session; patches are not self-contained |
Version bytes
Prefix application payloads with a format version for multi-format pipelines:
0x03 = Twilic v3 Dynamic
0x02 = Twilic v2 Dynamic
0x01 = legacy MessagePack (during migration)Decoders branch on the first byte. See Migrating from MessagePack.
v2 vs v3
v3 is a clean break from v2 for Bound Profile field/record-body payloads. Dynamic Profile may remain v2-compatible where tags are unchanged, but implementations that support both versions must use an explicit external profile and version discriminator. Payload auto-detection is not defined.
v1 vs v2
v2 is a clean break. A v2 decoder is not required to decode v1 payloads. Deploy v1 and v2 decoders in parallel during migration:
Common interop failures
| Symptom | Cause |
|---|---|
| Decode error on valid bytes | v1 payload sent to v2 decoder |
| Missing fields | Schema mismatch (Bound profile) |
| Patch decode failure | Session state not synchronized |
| Integer type mismatch | JS number vs bigint — use bigint for u64/i64 |
| Key order differences | Maps are unordered — compare semantically |
Integer handling across languages
| Language | u64/i64 type |
|---|---|
| JavaScript | bigint |
| Python | int |
| Go | uint64 / int64 via constructors |
| Rust | u64 / i64 enum variants |
| Java | long / BigInteger depending on SDK |
JavaScript encoders must use bigint for values outside Number.MAX_SAFE_INTEGER.