Schemantic
Schemantic is a general-purpose Dart library used for defining strongly typed data classes that automatically bind to reusable runtime JSON schemas. It is standard for the genkit-dart framework but works independently as well.
Core Concepts
Always use schemantic when strongly typed JSON parsing or programmatic schema validation is required.
- Annotate your abstract classes with
@Schema(). - Use the
$prefix for abstract schema class names (e.g.,abstract class $User). - Always run
dart run build_runner buildto generate the.g.dartschema files.
Basic Usage
- Defining a schema:
import 'package:schemantic/schemantic.dart';
part 'my_file.g.dart'; // Must match the filename
@Schema()
abstract class $MyObj {
String get name;
$MySubObj get subObj;
}
@Schema()
abstract class $MySubObj {
String get foo;
}
- Using the Generated Class:
The builder creates a concrete class MyObj (no $) with a factory constructor (MyObj.fromJson) and a regular constructor.
// Creating an instance
final obj = MyObj(name: 'test', subObj: MySubObj(foo: 'bar'));
// Serializing to JSON
print(obj.toJson());
// Parsing from JSON
final parsed = MyObj.fromJson({'name': 'test', 'subObj': {'foo': 'bar'}});
- Accessing Schemas at Runtime:
The generated data classes have a static $schema field (of type SchemanticType<T>) which can be used to pass the definition into functions or to extract the raw JSON schema.
// Access JSON schema
final schema = MyObj.$schema.jsonSchema;
print(schema.toJson());
// Validate arbitrary JSON at runtime
final validationErrors = await schema.validate({'invalid': 'data'});
Primitive Schemas
When a full data class is not required, Schemantic provides functions to create schemas dynamically.
final ageSchema = SchemanticType.integer(description: 'Age in years', minimum: 0);
final nameSchema = SchemanticType.string(minLength: 2);
final nothingSchema = SchemanticType.voidSchema();
final anySchema = SchemanticType.dynamicSchema();
final userSchema = SchemanticType.map(.string(), .integer()); // Map<String, int>
final tagsSchema = SchemanticType.list(.string()); // List<String>
Union Types (AnyOf)
To allow a field to accept multiple types, use @AnyOf.
@Schema()
abstract class $Poly {
@AnyOf([int, String, $MyObj])
Object? get id;
}
Schemantic generates a specific helper class (e.g., PolyId) to handle the values:
final poly1 = Poly(id: PolyId.int(123));
final poly2 = Poly(id: PolyId.string('abc'));
Field Annotations
You can use specialized annotations for more validation boundaries:
@Schema()
abstract class $User {
@IntegerField(
name: 'years_old', // Change JSON key
description: 'Age of the user',
minimum: 0,
defaultValue: 18,
)
int? get age;
@StringField(
minLength: 2,
enumValues: ['user', 'admin'],
)
String get role;
}
Recursive Schemas
For recursive structures (like trees), must use useRefs: true inside the generated jsonSchema property. You define it normally:
@Schema()
abstract class $Node {
String get id;
List<$Node>? get children;
}
Note: Node.$schema.jsonSchema(useRefs: true) generates schemas with JSON Schema $ref.