A general-purpose Dart library for generating type-safe data classes and runtime-accessible JSON Schemas from abstract class definitions.
Features
- Type-Safe Data Classes: Generates fully-typed Dart data classes from simple abstract definitions.
- Runtime JSON Schema: Access the standard JSON Schema for any generated type at runtime.
- Serialization: Built-in
toJsonandparsemethods. - Validation: Validate JSON data against the generated schema at runtime.
- Recursive Schemas: Easy support for recursive data structures (e.g., trees) using
$ref.
Installation
Add schemantic and build_runner to your pubspec.yaml:
dart pub add schemantic
dart pub add dev:build_runner
Usage
1. Define your specific Schema
Create a Dart file (e.g., user.dart) and define your schema as an abstract class annotated with @Schematic().
import 'package:schemantic/schemantic.dart';
part 'user.schema.g.dart';
@Schematic()
abstract class UserSchema {
String get name;
int? get age;
bool get isAdmin;
}
2. Generate Code
Run the build runner to generate the implementation:
dart run build_runner build
This will generate a user.schema.g.dart file containing:
User: The concrete data class.UserType: A utility class for parsing, schema access, and validation.
3. Use the Generated Types
You can now use the generated User class and UserType utility:
void main() async {
// Create an instance using the generated class
final user = User.from(
name: 'Alice',
age: 30,
isAdmin: true,
);
// Serialize to JSON
print(user.toJson());
// Output: {name: Alice, age: 30, isAdmin: true}
// Parse from JSON
final parsed = UserType.parse({
'name': 'Bob',
'isAdmin': false,
});
print(parsed.name); // Bob
// Access JSON Schema at runtime
final schema = UserType.jsonSchema();
print(schema.toJson());
// Output: {type: object, properties: {name: {type: string}, ...}, required: [name, isAdmin]}
// Validate data
final validation = await schema.validate({'name': 'Charlie'}); // Missing 'isAdmin'
if (validation.isNotEmpty) {
print('Validation errors: $validation');
}
}
Advanced
Recursive Schemas
For recursive structures like trees, use the useRefs: true option when generating the schema. This utilizes JSON Schema $ref to handle recursion.
@Schematic()
abstract class NodeSchema {
String get id;
List<NodeSchema>? get children;
}
void main() {
// Must use useRefs: true for recursive schemas
final schema = NodeType.jsonSchema(useRefs: true);
print(schema.toJson());
// Generates schema with "$ref": "#/$defs/Node"
}
Basic & Dynamic Types
Schemantic provides a set of basic types and helpers for creating dynamic schemas without generating code.
Primitives
stringType()intType()doubleType()boolType()voidType()
listType and mapType
You can create strongly typed Lists and Maps dynamically:
void main() {
// Define a List of Strings
final stringList = listType(stringType());
print(stringList.parse(['a', 'b'])); // ['a', 'b']
// Define a Map with String keys and Integer values
final scores = mapType(stringType(), intType());
print(scores.parse({'Alice': 100, 'Bob': 80})); // {'Alice': 100, 'Bob': 80}
// Nesting types
final matrix = listType(listType(intType()));
print(matrix.parse([[1, 2], [3, 4]])); // [[1, 2], [3, 4]]
// JSON Schema generation works as expected
print(scores.jsonSchema().toJson());
// {type: object, additionalProperties: {type: integer}}
}
## Schema Metadata
You can add a description to your generated schema using the `description` parameter in `@Schematic`:
```dart
@Schematic(description: 'Represents a user in the system')
abstract class UserSchema {
// ...
}
Enhanced Collections
You can use listType and mapType to create collections with metadata and validation:
// A list of strings with description and size constraints.
final tags = listType(
stringType(),
description: 'A list of tags',
minItems: 1,
maxItems: 10,
uniqueItems: true,
);
// A map with integer values.
final scores = mapType(
stringType(),
intType(),
description: 'Player scores',
minProperties: 1,
);
Basic Types
Schemantic provides factories for basic types with optional metadata:
stringType({String? description, int? minLength, ...})intType({String? description, int? minimum, ...})doubleType({String? description, double? minimum, ...})boolType({String? description})dynamicType({String? description})
Example:
final age = intType(
description: 'Age in years',
minimum: 0,
);
Customizing Fields
You can use specialized annotations to apply JSON Schema constraints directly to your Dart fields.
@Field: Basic customization (name, description).@StringField: Constraints for strings (minLength, maxLength, pattern, format, enumValues).@IntegerField: Constraints for integers (minimum, maximum, multipleOf).@NumberField: Constraints for doubles/numbers (minimum, maximum, multipleOf).
@Schematic()
abstract class UserSchema {
// Map 'age' to 'years_old' in JSON, and add validation
@IntegerField(
name: 'years_old',
description: 'Age of the user',
minimum: 0,
maximum: 120
)
int? get age;
@StringField(
minLength: 2,
maxLength: 50,
pattern: r'^[a-zA-Z\s]+$',
enumValues: ['user', 'admin'] // Mapped to 'enum' in JSON Schema
)
String get role;
}
Validation matches the Dart type (e.g., using @StringField on an int getter will throw a build-time error).
Schema-based Definition
Alternatively, you can define your schema using the Schema class directly. This is useful when you want to define the schema structure explicitly without an abstract class.
import 'package:schemantic/schemantic.dart';
part 'config.schema.g.dart';
// The variable name determines the generated class name (e.g. 'myConfig' -> 'MyConfig').
const myConfig = Schema.object(
properties: {
'host': Schema.string(),
'port': Schema.integer(minimum: 1, maximum: 65535),
'tags': Schema.list(items: Schema.string()),
'meta': Schema.map(valueType: Schema.string()), // or Schema.object(additionalProperties: Schema.string())
},
required: ['host', 'port'],
);
Run build_runner, and it will generate:
MyConfigclass (extension type)myConfigTypefactory and utility
Strict Generation
The generator enforces strict validation for Schema definitions:
- You must use
Schema.*static methods (e.g.,Schema.object,Schema.string). - Property keys must be string literals.
- Unknown arguments will throw an error during generation.