What gets generated
For each class annotated with @luthor, the generator writes five items into the part file: a generated schema, a validate function, a validateSelf extension, schema keys, and error keys. This page shows all five for one model. The generated code is copied from a real build_runner run on the model below. The pages that follow use the same User and Address.
The models
Section titled “The models”import 'package:freezed_annotation/freezed_annotation.dart';import 'package:luthor/luthor.dart';
part 'user.freezed.dart';part 'user.g.dart';
enum Role { admin, member }
@luthor@freezedabstract class User with _$User { const factory User({ @HasMin(2) required String name, @IsEmail() @JsonKey(name: 'email_address') required String email, @HasMin(18) int? age, @Default(Role.member) Role role, required Address address, }) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);}
@luthor@freezedabstract class Address with _$Address { const factory Address({ required String street, required String city, @MatchRegex(r'^\d{5}$') required String postcode, }) = _Address;
factory Address.fromJson(Map<String, dynamic> json) => _$AddressFromJson(json);}Generated schema
Section titled “Generated schema”$UserSchema is a final SchemaValidator. Each field’s validator is derived from the parameter type and its annotations; Supported field types lists the mapping. The enum becomes l.oneOf() of its names, the default makes role optional, and the nested model is referenced through forwardRef(). The schema is named after the model with withName.
final SchemaValidator $UserSchema = l .schema({ UserSchemaKeys.name: l.string().min(2).required(), UserSchemaKeys.email: l.string().email().required(), UserSchemaKeys.age: l.int().min(18), UserSchemaKeys.role: l.oneOf(["admin", "member"]), UserSchemaKeys.address: forwardRef(() => $AddressSchema.required()), }) .withName("User");final SchemaValidator $AddressSchema = l .schema({ AddressSchemaKeys.street: l.string().required(), AddressSchemaKeys.city: l.string().required(), AddressSchemaKeys.postcode: l .string() .regex(RegExp(r'^\d{5}$')) .required(), }) .withName("Address");Validate function
Section titled “Validate function”$UserValidate accepts any input and runs validateSchema with the model’s fromJson, so success carries a User.
ValidationResult<User> $UserValidate(Object? json) => $UserSchema.validateSchema(json, fromJson: User.fromJson);For a dart_mappable model the function uses UserMapper.fromMap instead.
$UserValidate(input)+1-1- valid:
{'name': 'Ada', 'email_address': '[email protected]', 'age': 36, 'role': 'admin', 'address': {...}} - invalid:
{'name': 'A', 'email_address': 'ada', 'age': 12, 'role': 'owner', 'address': {..., 'postcode': '12'}}
Input that is not a map at a nested field reports with the field’s name:
{address: [address must be a map]}validateSelf extension
Section titled “validateSelf extension”The extension serialises an instance with toJson(), converts any nested objects left as instances, and validates the result. It is how you check a model you built in code rather than parsed from input. It is generated only for models with a toJson() or a dart_mappable toMap().
extension UserValidationExtension on User { ValidationResult<User> validateSelf() => $UserValidate(_$luthorJsonMap(toJson()));}final user = User( name: 'A', email: 'ada', address: const Address(street: 's', city: 'c', postcode: '12345'),);print(user.validateSelf());ValidationFailure(errors: {name: [name must be at least 2 characters long], email_address: [email_address must be a valid email address]})Schema keys
Section titled “Schema keys”UserSchemaKeys is a record. Each field is named after the Dart parameter and holds the key as it appears in the input map, which is the @JsonKey name when there is one. The generated schema uses these keys.
const UserSchemaKeys = ( name: "name", email: "email_address", age: "age", role: "role", address: "address",);Error keys
Section titled “Error keys”UserErrorKeys has the same fields, but each holds an error path for getError. A field whose type is another model becomes a nested record whose paths carry the prefix, with $key for the field’s own path.
const UserErrorKeys = ( name: "name", email: "email_address", age: "age", role: "role", address: ( $key: "address", street: "address.street", city: "address.city", postcode: "address.postcode", ),);print(result.getError(UserErrorKeys.email));print(result.getError(UserErrorKeys.address.postcode));print(result.getError(UserErrorKeys.address.$key));print(UserSchemaKeys.email);print(UserErrorKeys.address.$key);email_address must be a valid email addresspostcode must match the pattern ^\d{5}$nullemail_addressaddressSchema keys and error keys goes into both records.
Unknown keys
Section titled “Unknown keys”A generated schema strips keys it does not declare before fromJson runs, like every 1.0 schema, so input with extra keys validates and the extras never reach the serializer:
trueEvery key the serializer reads has to be a schema field, and the generator makes it one: for a @JsonSerializable class, public settable fields that are not constructor parameters are schema fields too. When the serializer may read keys the generator cannot name, the schema gets .passthrough() instead: a field with @JsonKey(readValue:), a private field with @JsonKey(includeFromJson: true), or a @MappableClass(hook:).
Auto-generated schemas
Section titled “Auto-generated schemas”When a field’s type is a class without @luthor that has a fromJson or @MappableClass and a constructor with named parameters, the generator emits a private schema for it, _$ClassSchema, once per library. It gets no public keys, validate function or extension. Annotate the class with @luthor to get the public API.
Which constructor
Section titled “Which constructor”The schema follows the constructor the serializer uses: the one named in @JsonSerializable(constructor:), the @MappableConstructor, the unnamed constructor, or else the first public constructor with parameters.