Defining Schemas
Luthor has a concept of “schemas” which are used to define the structure of the data that you want to validate. Essentially, a schema is a Map that defines the structure of the data that you want to validate. The keys of the Map are the names of the fields that you want to validate, and the values are the rules that you want to apply to those fields.
Manual Schema Definition
Here’s an example of a simple schema:
import 'package:luthor/luthor.dart';
void main() { final person = l.schema({ 'name': l.string().min(1).required(), 'age': l.int().required(), });
person.validateSchema({ 'name': 'John Doe', 'age': 30, });}Above, we define a schema for a person object that has two fields: name and age. The name field is a required string that must have a minimum length of 1, and the age field is a required integer.
You can also define nested schemas:
import 'package:luthor/luthor.dart';
void main() { final person = l.schema({ 'name': l.string().min(1).required(), 'age': l.int().required(), 'address': l.schema({ 'street': l.string().required(), 'city': l.string().required(), 'zip': l.string().required(), }).required(), });
person.validateSchema({ 'name': 'John Doe', 'age': 30, 'address': { 'street': '123 Main St', 'city': 'Springfield', 'zip': '12345', }, });}Code Generation with SchemaKeys
When using code generation, Luthor automatically generates type-safe SchemaKeys that provide compile-time safety and IDE support for schema definition. This eliminates typos and makes refactoring safer.
Basic Schema with Code Generation
import 'package:freezed_annotation/freezed_annotation.dart';import 'package:luthor/luthor.dart';
part 'person.freezed.dart';part 'person.g.dart';
@luthor@freezedabstract class Person with _$Person { const factory Person({ @HasMin(1) required String name, required int age, }) = _Person;
factory Person.fromJson(Map<String, dynamic> json) => _$PersonFromJson(json);}
void main() { final result = $PersonValidate({ 'name': 'John Doe', 'age': 30, });
print(result);}Generated SchemaKeys
The code generator automatically creates SchemaKeys constants:
// Generated codeconst PersonSchemaKeys = ( name: "name", age: "age",);
// Generated schema using SchemaKeys for type safetyValidator $PersonSchema = l.withName('Person').schema({ PersonSchemaKeys.name: l.string().min(1).required(), PersonSchemaKeys.age: l.int().required(),});Nested Schemas with Code Generation
@luthor@freezedabstract class Address with _$Address { const factory Address({ required String street, required String city, required String zip, }) = _Address;
factory Address.fromJson(Map<String, dynamic> json) => _$AddressFromJson(json);}
@luthor@freezedabstract class PersonWithAddress with _$PersonWithAddress { const factory PersonWithAddress({ @HasMin(1) required String name, required int age, required Address address, }) = _PersonWithAddress;
factory PersonWithAddress.fromJson(Map<String, dynamic> json) => _$PersonWithAddressFromJson(json);}
void main() { final result = $PersonWithAddressValidate({ 'name': 'John Doe', 'age': 30, 'address': { 'street': '123 Main St', 'city': 'Springfield', 'zip': '12345', }, });
print(result);}Benefits of SchemaKeys
- Type Safety - No more string typos in field names
- IDE Support - Full autocomplete and go-to-definition
- Refactoring Safety - Renaming fields updates keys automatically
- JsonKey Support - Automatically respects
@JsonKeyannotations
JsonKey Integration
SchemaKeys automatically handle @JsonKey annotations:
@luthor@freezedabstract class ApiPerson with _$ApiPerson { const factory ApiPerson({ @JsonKey(name: 'full_name') @HasMin(1) required String name, @JsonKey(name: 'user_age') required int age, }) = _ApiPerson;
factory ApiPerson.fromJson(Map<String, dynamic> json) => _$ApiPersonFromJson(json);}
// Generated SchemaKeys use JsonKey namesconst ApiPersonSchemaKeys = ( name: "full_name", // Uses JsonKey name age: "user_age", // Uses JsonKey name);
// Schema validation uses JSON field namesfinal result = $ApiPersonValidate({ 'full_name': 'John Doe', 'user_age': 30,});Advanced Schema Features
Named Schemas
You can create named schemas for better error messages and debugging:
final userSchema = l.withName('User').schema({ 'email': l.string().email().required(), 'password': l.string().min(8).required(),});Optional vs Required Fields
By default, fields in schemas are optional. Use .required() to make them mandatory:
final schema = l.schema({ 'required_field': l.string().required(), // Must be present 'optional_field': l.string(), // Can be null/missing});Schema Reuse
Schemas can be reused and composed:
final addressSchema = l.schema({ 'street': l.string().required(), 'city': l.string().required(), 'zip': l.string().required(),});
final personSchema = l.schema({ 'name': l.string().required(), 'home_address': addressSchema.required(), 'work_address': addressSchema, // Optional});Self-Referential Schemas with forwardRef()
When defining schemas that reference themselves (like a Node class with List<Node>? children), use forwardRef() to prevent stack overflow errors during schema construction:
late Validator nodeSchema;nodeSchema = l.schema({ 'value': l.string().required(), 'children': l.list( validators: [forwardRef(() => nodeSchema.required())], ),});The forwardRef() function defers validator resolution until validation time, allowing recursive schema definitions without causing stack overflow during construction.
When to use forwardRef():
- Self-referential types:
Node? parent,List<Node>? children - Circular references within the same schema
- Any schema that references itself during definition
Example - Tree Structure:
late Validator treeNodeSchema;treeNodeSchema = l.schema({ 'id': l.string().required(), 'value': l.string().required(), 'parent': forwardRef(() => treeNodeSchema), // Optional parent reference 'children': l.list( validators: [forwardRef(() => treeNodeSchema.required())], ), // List of child nodes});Schema Validation with Deserialization
When using code generation, you can validate and deserialize in one step:
class User { final String name; final String email;
User({required this.name, required this.email});
factory User.fromJson(Map<String, dynamic> json) => User( name: json['name'] as String, email: json['email'] as String, );}
final result = userSchema.validateSchema( fromJson: User.fromJson,);
switch (result) { case SchemaValidationSuccess(data: final User user): print('User: ${user.name}'); case SchemaValidationError(): print('Validation failed');}// Using generated validation functionfinal result = $UserValidate({ 'name': 'John',});
switch (result) { case SchemaValidationSuccess(data: final User user): print('User: ${user.name}'); // Already deserialized! case SchemaValidationError(): print('Validation failed');}Schemas provide a powerful way to validate complex nested data structures while maintaining type safety and clear validation rules.