Skip to content

Variant Type

Variant is a sealed discriminated union representing a value that is exactly one of: None, Bool, Int32, String, Dictionary, or a nested array. It is the lingua franca of the platform: every cross-process value (gRPC request bodies, state-store entries, message payloads, worker code) is a Variant.

The cases

classDiagram
  class Variant {
    <<sealed abstract record>>
    +None() Variant
    +Bool(bool) Variant
    +Int32(int) Variant
    +String(string) Variant
    +Dictionary(IReadOnlyDictionary~string,Variant~) Variant
    +Array(IReadOnlyList~Variant~) Variant
    +Serialize() byte[]
    +Deserialize(byte[]) Variant
  }
  class Variant~T~ {
    <<record>>
    +Value: T
  }

Variant is an abstract record with 5 concrete subtypes (one per case). The 6th case, None, is a singleton (Variant.None).

Construction

using Virtufin.Data;

// Explicit construction
Variant v1 = Variant.Int32(42);
Variant v2 = Variant.String("hello");
Variant v3 = Variant.Bool(true);
Variant v4 = Variant.None();
Variant v5 = Variant.Dictionary(new Dictionary<string, Variant>
{
    ["price"] = 100.5m,
    ["symbol"] = "BTCUSDT",
    ["sides"] = Variant.Array(new[] { Variant.String("buy"), Variant.String("sell") }),
});

// Implicit construction from C# primitives
Variant implicit1 = 42;            // -> Variant.Int32(42)
Variant implicit2 = "hello";       // -> Variant.String("hello")
Variant implicit3 = true;          // -> Variant.Bool(true)
Variant implicit4 = 100.5m;        // -> Variant.Decimal via implicit conversion

Pattern matching

string describe(Variant v) => v switch
{
    Variant.None => "null",
    Variant.Bool b => $"bool: {b.Value}",
    Variant.Int32 i => $"int: {i.Value}",
    Variant.String s => $"str: {s.Value}",
    Variant.Dictionary d => $"dict with {d.Value.Count} keys",
    Variant.Array a => $"array with {a.Value.Count} elements",
    _ => "unknown",  // unreachable — Variant is sealed
};

Type safety

Variant is sealed, so the C# compiler can verify exhaustiveness in switch expressions. The implicit conversions from C# primitives are lossy in one direction: a long or double will be coerced to Int32 (with potential data loss). For arbitrary-precision decimals, use the explicit Variant.Decimal(DecimalAmount) constructor.

// These are SAFE
Variant.Int32(42);
Variant.String("hello");

// These COERCE — be careful
long big = 1_000_000_000_000L;
Variant v = big;  // => Variant.Int32(1_000_000_000_000) — fits, but read as int32

FlatBuffers serialization

Every Variant can be serialized to FlatBuffers binary and deserialized back. The schema (virtufin/data/variant.fbs) is shared across C#, Python, and TypeScript SDKs — a value produced in C# is readable in Python without any code generation.

Variant v = Variant.Dictionary(new Dictionary<string, Variant>
{
    ["price"] = 100.5m,
    ["symbol"] = "BTCUSDT",
});

byte[] bytes = v.Serialize();
int size = bytes.Length;  // ~50 bytes for this example

// On the receiving end (any language)
Variant restored = Variant.Deserialize(bytes);

The FlatBuffers schema is in Virtufin.Data/Schemas/variant.fbs. See FlatBuffers for the cross-language contract.

Equality and hashing

Variant uses record-style value equality: two Variants are equal if they're the same case and have equal inner values. Dictionary and Array cases use sequence-equal comparison. This means Variants can be used as dictionary keys or in HashSets.

var a = Variant.Int32(42);
var b = Variant.Int32(42);
Console.WriteLine(a == b);  // True

var x = Variant.Dictionary(new Dictionary<string, Variant> { ["k"] = Variant.Int32(1) });
var y = Variant.Dictionary(new Dictionary<string, Variant> { ["k"] = Variant.Int32(1) });
Console.WriteLine(x == y);  // True (sequence-equal)

Cross-cutting use cases

Surface Why Variant
gRPC state-store values gRPC doesn't have a "dynamic" type — Variant is the dynamic
Worker code payloads Workers accept a Variant body in their Execute call; the Variant shape defines the worker's contract
Pub/Sub message bodies CloudEvent data field is Variant so any shape can flow through the bus
Configuration values All config (env, file, command-line) is read as Variant and dispatched by type

Performance characteristics

  • Variant is an abstract record with 5 concrete subtypes — no boxing for value types (they're stored as the inner value).
  • Serialize allocates a byte[] of ~20-100 bytes depending on complexity.
  • Deserialize parses a FlatBuffer — O(n) in payload size, no reflection.
  • Dictionary and Array cases use immutable inner types (IReadOnlyDictionary, IReadOnlyList) so they're safe to share across threads.

For very high-throughput paths, the DecimalAmount case (the 6th case, not in the diagram above) is a struct that stores (long Whole, int Fraction, byte N) — no allocations, copy-by-value.