C# SDK Reference¶
Installation¶
dotnet add package AnyVali
Quick Start¶
using AnyVali;
// Define a schema
var userSchema = V.Object(new Dictionary<string, Schema>
{
["name"] = V.String().MinLength(1).MaxLength(100),
["email"] = V.String().Format("email"),
["age"] = V.Optional(V.Int().Min(0).Max(150)),
});
// Parse input (throws on failure)
var user = userSchema.Parse(new Dictionary<string, object?>
{
["name"] = "Alice",
["email"] = "alice@example.com",
["age"] = 30,
});
// Safe parse (returns result object)
var result = userSchema.SafeParse(new Dictionary<string, object?>
{
["name"] = "",
["email"] = "bad",
});
if (!result.Success)
{
foreach (var issue in result.Issues)
{
Console.WriteLine($"{string.Join(".", issue.Path)}: [{issue.Code}] {issue.Message}");
}
}
Schema Builders¶
All schemas are created through static methods on the V class.
Primitive Types¶
using AnyVali;
var str = V.String();
var num = V.Number(); // float64
var integer = V.Int(); // int64
var flag = V.Bool();
var nil = V.Null();
Numeric Types¶
AnyVali provides explicit numeric schemas for cross-language safety.
// Floating point
var f32 = V.Float32();
var f64 = V.Float64();
var number = V.Number(); // alias for float64
// Signed integers
var i = V.Int(); // int64 (default)
var i8 = V.Int8(); // -128 to 127
var i16 = V.Int16(); // -32,768 to 32,767
var i32 = V.Int32(); // -2^31 to 2^31-1
var i64 = V.Int64(); // -2^63 to 2^63-1
// Unsigned integers
var u8 = V.Uint8(); // 0 to 255
var u16 = V.Uint16(); // 0 to 65,535
var u32 = V.Uint32(); // 0 to 4,294,967,295
var u64 = V.Uint64(); // 0 to int64 max (capped for safety)
Special Types¶
var any = V.Any(); // accepts any value
var unknown = V.Unknown(); // accepts any value (stricter intent)
var never = V.Never(); // always fails
Literal and Enum¶
// Literal: must be exactly this value
var status = V.Literal("active");
// Enum: must be one of these values
var role = V.Enum("admin", "user", "guest");
Collections¶
// Array of strings
var tags = V.Array(V.String());
// Tuple: fixed-length, each position typed
var point = V.Tuple(V.Number(), V.Number());
// Record: string keys, all values share a schema
var scores = V.Record(V.Int());
Object¶
var user = V.Object(new Dictionary<string, Schema>
{
["name"] = V.String().MinLength(1),
["email"] = V.String().Format("email"),
["age"] = V.Optional(V.Int().Min(0)),
});
By default, unknown keys are rejected. Pass UnknownKeyMode to change this:
// Strip unknown keys silently
var loose = V.Object(
new Dictionary<string, Schema>
{
["id"] = V.Int(),
},
UnknownKeyMode.Strip
);
// Allow unknown keys in output
var passthrough = V.Object(
new Dictionary<string, Schema>
{
["id"] = V.Int(),
},
UnknownKeyMode.Allow
);
Composition¶
// Union: value must match at least one variant
var stringOrInt = V.Union(V.String(), V.Int());
// Intersection: value must match all schemas
var named = V.Intersection(
V.Object(new Dictionary<string, Schema> { ["name"] = V.String() }),
V.Object(new Dictionary<string, Schema> { ["age"] = V.Int() })
);
Modifiers¶
// Optional: field may be absent
var maybeAge = V.Optional(V.Int());
// Nullable: field may be null
var nullableName = V.Nullable(V.String());
String Constraints¶
var schema = V.String()
.MinLength(1) // minimum character count
.MaxLength(255) // maximum character count
.Pattern(@"^\w+$") // regex pattern
.StartsWith("hello") // must start with prefix
.EndsWith(".com") // must end with suffix
.Includes("@") // must contain substring
.Format("email"); // built-in format validator
Each constraint method returns a new schema instance -- schemas are immutable.
Built-in Formats¶
The .Format() method supports these built-in format validators:
"email"-- RFC 5322 email address"uri"-- absolute URI"uuid"-- UUID v4"iso8601"-- ISO 8601 date-time"ipv4"-- IPv4 address"ipv6"-- IPv6 address
var email = V.String().Format("email");
var uri = V.String().Format("uri");
var id = V.String().Format("uuid");
Numeric Constraints¶
Constraints apply to all numeric schemas (NumberSchema, IntSchema, and their subtypes).
var price = V.Number()
.Min(0) // value >= 0
.Max(10000) // value <= 10000
.ExclusiveMin(0) // value > 0
.ExclusiveMax(10000) // value < 10000
.MultipleOf(0.01); // must be a multiple of 0.01
var port = V.Int()
.Min(1)
.Max(65535);
var evenNumber = V.Int().MultipleOf(2);
Array Constraints¶
var tags = V.Array(V.String())
.MinItems(1) // at least 1 element
.MaxItems(10); // at most 10 elements
Parsing¶
Parse (Throwing)¶
Parse() returns the validated value or throws a ValidationError.
try
{
// Untyped parse: returns object?
object? value = V.String().Parse("hello");
// Typed parse via Schema<T>: returns T
string name = V.String().Parse("Alice");
long age = V.Int().Parse(30);
double price = V.Number().Parse(9.99);
bool flag = V.Bool().Parse(true);
}
catch (ValidationError ex)
{
foreach (var issue in ex.Issues)
{
Console.WriteLine($"[{issue.Code}] {issue.Message}");
}
}
SafeParse (Non-Throwing)¶
SafeParse() returns a ParseResult that never throws.
ParseResult result = V.String().SafeParse(42);
if (result.Success)
{
Console.WriteLine($"Parsed: {result.Data}");
}
else
{
foreach (var issue in result.Issues)
{
Console.WriteLine($"[{issue.Code}] {issue.Message}");
// [invalid_type] Expected string, received number
}
}
SafeParseTyped (Strongly Typed)¶
SafeParseTyped() is available on Schema<T> and returns ParseResult<T> with typed Data.
ParseResult<string> result = V.String().SafeParseTyped("hello");
if (result.Success)
{
string value = result.Data!; // no cast needed
Console.WriteLine(value.ToUpper());
}
ParseResult<Dictionary<string, object?>> userResult = V.Object(
new Dictionary<string, Schema>
{
["name"] = V.String(),
["age"] = V.Int(),
}
).SafeParseTyped(new Dictionary<string, object?>
{
["name"] = "Alice",
["age"] = 30,
});
if (userResult.Success)
{
Dictionary<string, object?> user = userResult.Data!;
Console.WriteLine(user["name"]);
}
Defaults and Coercion¶
Defaults¶
Defaults fill in missing (absent) values. They run after coercion and before validation. Call .Default(value) on any schema.
The default only applies when the value is absent -- if a value is present, it is validated normally. Defaults must be static values (for portability across SDKs).
var role = V.String().Default("user");
role.Parse(null); // => "user" (absent value filled)
role.Parse("admin"); // => "admin"
var count = V.Int().Default(0);
Defaults work with optional fields in objects:
var config = V.Object(new Dictionary<string, Schema>
{
["theme"] = V.Optional(V.String().Default("light")),
["language"] = V.Optional(V.String().Default("en")),
});
config.Parse(new Dictionary<string, object?>());
// => { "theme": "light", "language": "en" }
If the default value itself fails validation, a default_invalid issue is produced.
Coercion¶
Coercion transforms the input value before validation. It runs only when the value is present. Call .Coerce(config) on any schema to enable coercion.
Available Coercions¶
String to Integer
var age = V.Int().Coerce(new CoercionConfig { From = "string" });
age.Parse("42"); // => 42L (string coerced to integer)
age.Parse(42); // => 42L (already an integer, no coercion needed)
String to Number
var price = V.Number().Coerce(new CoercionConfig { From = "string" });
price.Parse("3.14"); // => 3.14
String to Boolean
var flag = V.Bool().Coerce(new CoercionConfig { From = "string" });
flag.Parse("true"); // => true
flag.Parse("false"); // => false
flag.Parse("1"); // => true
flag.Parse("0"); // => false
Trim Whitespace
var trimmed = V.String().Coerce(new CoercionConfig { Trim = true });
trimmed.Parse(" hello "); // => "hello"
Lowercase / Uppercase
var lower = V.String().Coerce(new CoercionConfig { Lower = true });
lower.Parse("HELLO"); // => "hello"
var upper = V.String().Coerce(new CoercionConfig { Upper = true });
upper.Parse("hello"); // => "HELLO"
Transformations can be combined:
var normalized = V.String().Coerce(new CoercionConfig { Trim = true, Lower = true });
normalized.Parse(" Hello World "); // => "hello world"
Export and Import¶
Schemas can be exported to a canonical JSON document and imported back, enabling cross-language schema sharing.
Export¶
var schema = V.Object(new Dictionary<string, Schema>
{
["name"] = V.String().MinLength(1),
["age"] = V.Int().Min(0),
});
// Export to AnyValiDocument
AnyValiDocument doc = schema.Export();
// Or via the static helper
AnyValiDocument doc2 = V.Export(schema);
// With explicit mode
AnyValiDocument portable = schema.Export(ExportMode.Portable);
AnyValiDocument extended = schema.Export(ExportMode.Extended);
The exported AnyValiDocument contains:
AnyvaliVersion-- protocol version ("1.0")SchemaVersion-- schema format version ("1")Root-- the schema node treeDefinitions-- named reusable schema definitionsExtensions-- language-specific extension metadata
Import¶
// Import an AnyValiDocument back into a live schema
Schema imported = V.Import(doc);
// Use it like any other schema
var result = imported.SafeParse(new Dictionary<string, object?>
{
["name"] = "Bob",
["age"] = 25,
});
Cross-Language Example¶
Export a schema in C#, use it in another language:
// C# side: define and export
var schema = V.Object(new Dictionary<string, Schema>
{
["id"] = V.Int64(),
["email"] = V.String().Format("email"),
});
AnyValiDocument doc = schema.Export(ExportMode.Portable);
// Serialize doc to JSON and send to another service
The resulting JSON document can be imported by any AnyVali SDK (JavaScript, Python, Go, Java, Kotlin, etc.).
Type Inference¶
The C# SDK uses generic base classes to provide static type inference. Schema<T> subclasses carry their output type:
| Schema Class | Output Type (T) |
|---|---|
StringSchema |
string |
NumberSchema |
double |
IntSchema |
long |
BoolSchema |
bool |
ObjectSchema |
Dictionary<string, object?> |
ArraySchema |
List<object?> |
When you call Parse() on a typed schema, the return type is T -- no cast required:
string name = V.String().Parse("Alice"); // returns string
long age = V.Int().Parse(30); // returns long
double price = V.Number().Parse(9.99); // returns double
bool active = V.Bool().Parse(true); // returns bool
Dictionary<string, object?> obj = V.Object(
new Dictionary<string, Schema>
{
["x"] = V.Int(),
}
).Parse(new Dictionary<string, object?> { ["x"] = 1 }); // returns Dictionary<string, object?>
Validation Issues¶
When parsing fails, you get a list of ValidationIssue objects with structured error information.
var schema = V.Object(new Dictionary<string, Schema>
{
["name"] = V.String().MinLength(1),
["age"] = V.Int().Min(0).Max(150),
});
var result = schema.SafeParse(new Dictionary<string, object?>
{
["name"] = "",
["age"] = 200,
});
foreach (var issue in result.Issues)
{
Console.WriteLine($"Path: {string.Join(".", issue.Path)}");
Console.WriteLine($"Code: {issue.Code}");
Console.WriteLine($"Message: {issue.Message}");
Console.WriteLine($"Expected: {issue.Expected}");
Console.WriteLine($"Received: {issue.Received}");
Console.WriteLine();
}
// Path: name
// Code: too_small
// Message: String must have at least 1 character(s)
// Expected: 1
// Received: 0
//
// Path: age
// Code: too_large
// Message: Number must be <= 150
// Expected: 150
// Received: 200
Issue Codes¶
| Code | Meaning |
|---|---|
invalid_type |
Value is the wrong type |
too_small |
Value is below minimum |
too_large |
Value is above maximum |
invalid_string |
String constraint violated (pattern, format, etc.) |
invalid_number |
Numeric constraint violated (multipleOf, etc.) |
required |
Required object property is missing |
unknown_key |
Object has an unrecognized key |
coercion_failed |
Coercion could not convert the value |
default_invalid |
Default value failed schema validation |
Common Patterns¶
Validating Environment Variables¶
Use UnknownKeyMode.Strip when parsing objects that contain many extra keys you don't care about, like environment variables:
var envSchema = V.Object(new Dictionary<string, Schema>
{
["NODE_ENV"] = V.Optional(V.String()).WithDefault("development"),
["DATABASE_URL"] = V.String(),
}, UnknownKeyMode.Strip);
Without Strip, parse would fail with unknown_key issues for every other variable in the environment (PATH, HOME, etc.) because the default mode is Reject.
| Mode | What happens with extra keys |
|---|---|
Reject (default) |
Parse fails with unknown_key issues |
Strip |
Extra keys silently removed from output |
Allow |
Extra keys passed through to output |
Eagerly Evaluated vs Lazy Defaults¶
.WithDefault() accepts any value of the correct type. Expressions like Directory.GetCurrentDirectory() are evaluated immediately when the schema is created and stored as a static value -- this works fine. What AnyVali does not support is lazy delegate defaults that re-evaluate on each parse call. If you need a fresh value on every parse, apply it after:
var configSchema = V.Object(new Dictionary<string, Schema>
{
["profile"] = V.Optional(V.String()).WithDefault("default"),
["appDir"] = V.Optional(V.String()),
}, UnknownKeyMode.Strip);
var config = (Dictionary<string, object?>)configSchema.Parse(data);
config["appDir"] ??= Directory.GetCurrentDirectory();
This keeps the schema fully portable -- the same JSON document can be imported in Go, Python, or any other SDK without relying on language-specific function calls.
API Reference¶
V (Static Factory)¶
| Method | Returns | Description |
|---|---|---|
V.String() |
StringSchema |
String schema |
V.Number() |
NumberSchema |
Float64 number schema |
V.Float32() |
Float32Schema |
Float32 schema |
V.Float64() |
Float64Schema |
Float64 schema |
V.Int() |
IntSchema |
Int64 integer schema |
V.Int8() |
Int8Schema |
Signed 8-bit integer |
V.Int16() |
Int16Schema |
Signed 16-bit integer |
V.Int32() |
Int32Schema |
Signed 32-bit integer |
V.Int64() |
Int64Schema |
Signed 64-bit integer |
V.Uint8() |
Uint8Schema |
Unsigned 8-bit integer |
V.Uint16() |
Uint16Schema |
Unsigned 16-bit integer |
V.Uint32() |
Uint32Schema |
Unsigned 32-bit integer |
V.Uint64() |
Uint64Schema |
Unsigned 64-bit integer |
V.Bool() |
BoolSchema |
Boolean schema |
V.Null() |
NullSchema |
Null schema |
V.Any() |
AnySchema |
Accepts any value |
V.Unknown() |
UnknownSchema |
Accepts any value (stricter intent) |
V.Never() |
NeverSchema |
Always fails |
V.Literal(value) |
LiteralSchema |
Exact value match |
V.Enum(values) |
EnumSchema |
One of the listed values |
V.Array(items) |
ArraySchema |
Array with item schema |
V.Tuple(items) |
TupleSchema |
Fixed-length typed array |
V.Object(shape, unknownKeys?) |
ObjectSchema |
Object with property schemas |
V.Record(valueSchema) |
RecordSchema |
String-keyed record |
V.Union(variants) |
UnionSchema |
At least one variant must match |
V.Intersection(schemas) |
IntersectionSchema |
All schemas must match |
V.Optional(inner) |
OptionalSchema |
Value may be absent |
V.Nullable(inner) |
NullableSchema |
Value may be null |
V.Parse(schema, input) |
object? |
Parse via static helper |
V.SafeParse(schema, input) |
ParseResult |
Safe parse via static helper |
V.Export(schema, mode?) |
AnyValiDocument |
Export schema to document |
V.Import(doc) |
Schema |
Import document to live schema |
Schema (Base Class)¶
| Member | Signature | Description |
|---|---|---|
Parse |
object? Parse(object? input) |
Parse or throw ValidationError |
SafeParse |
ParseResult SafeParse(object? input) |
Parse without throwing |
Default |
Schema Default(object? value) |
Set a default value |
Coerce |
Schema Coerce(CoercionConfig? config) |
Enable coercion |
Export |
AnyValiDocument Export(ExportMode mode) |
Export to document |
Schema<T> (Generic Base)¶
| Member | Signature | Description |
|---|---|---|
Parse |
T Parse(object? input) |
Typed parse or throw |
SafeParseTyped |
ParseResult<T> SafeParseTyped(object? input) |
Typed safe parse |
ParseResult¶
| Property | Type | Description |
|---|---|---|
Success |
bool |
Whether parsing succeeded |
Data |
object? |
Parsed value (null on failure) |
Issues |
IReadOnlyList<ValidationIssue> |
List of issues (empty on success) |
ParseResult<T>¶
| Property | Type | Description |
|---|---|---|
Success |
bool |
Whether parsing succeeded |
Data |
T? |
Typed parsed value (default on failure) |
Issues |
IReadOnlyList<ValidationIssue> |
List of issues (empty on success) |
ValidationIssue¶
| Property | Type | Description |
|---|---|---|
Code |
string |
Issue code (e.g. "invalid_type") |
Message |
string |
Human-readable message |
Path |
List<object> |
Path to the issue (strings and ints) |
Expected |
string? |
What was expected |
Received |
string? |
What was received |
Meta |
Dictionary<string, object>? |
Optional metadata |
StringSchema Constraints¶
| Method | Parameter | Description |
|---|---|---|
.MinLength(n) |
int |
Minimum string length |
.MaxLength(n) |
int |
Maximum string length |
.Pattern(p) |
string |
Regex pattern |
.StartsWith(s) |
string |
Required prefix |
.EndsWith(s) |
string |
Required suffix |
.Includes(s) |
string |
Required substring |
.Format(f) |
string |
Built-in format validator |
NumberSchema / IntSchema Constraints¶
| Method | Parameter | Description |
|---|---|---|
.Min(n) |
double |
Minimum value (inclusive) |
.Max(n) |
double |
Maximum value (inclusive) |
.ExclusiveMin(n) |
double |
Minimum value (exclusive) |
.ExclusiveMax(n) |
double |
Maximum value (exclusive) |
.MultipleOf(n) |
double |
Must be a multiple of this value |
ArraySchema Constraints¶
| Method | Parameter | Description |
|---|---|---|
.MinItems(n) |
int |
Minimum number of elements |
.MaxItems(n) |
int |
Maximum number of elements |
UnknownKeyMode Enum¶
| Value | Description |
|---|---|
UnknownKeyMode.Reject |
Reject unknown keys (default) |
UnknownKeyMode.Strip |
Remove unknown keys from output |
UnknownKeyMode.Allow |
Pass unknown keys through |
ExportMode Enum¶
| Value | Description |
|---|---|
ExportMode.Portable |
Fails if schema contains non-portable features |
ExportMode.Extended |
Emits core schema plus extension namespaces |