Models without freezed
The generator needs two things from a class: a constructor with named parameters, and a way to convert a map to an instance. freezed supplies both, but it is not required. This page shows the three other setups with the same User model, and the generated code for each.
What the generator needs
Section titled “What the generator needs”- The class is annotated with
@luthor. - A constructor with named parameters. 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. - Either a
fromJsonfactory, generative constructor or static method, or the@MappableClassannotation fromdart_mappable. - For
validateSelf, atoJson()method ordart_mappable’stoMap(). Without one, the extension is not generated.
No build.yaml is needed. The builder applies source_gen’s combining builder, so a .g.dart is produced even without json_serializable.
Setups
Section titled “Setups”dart pub add luthor json_annotationdart pub add dev:build_runner dev:luthor_generator dev:json_serializableimport 'package:json_annotation/json_annotation.dart';import 'package:luthor/luthor.dart';
part 'user.g.dart';
@luthor@JsonSerializable(fieldRename: FieldRename.snake)class User { const User({required this.fullName, required this.email, this.age});
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
@HasMin(2) final String fullName; @IsEmail() final String email; @HasMin(18) final int? age;
Map<String, dynamic> toJson() => _$UserToJson(this);}Annotations on fields are read, and fieldRename applies to the keys:
const UserSchemaKeys = (fullName: "full_name", email: "email", age: "age");
final SchemaValidator $UserSchema = l .schema({ UserSchemaKeys.fullName: l.string().min(2).required(), UserSchemaKeys.email: l.string().email().required(), UserSchemaKeys.age: l.int().min(18), }) .withName("User");dart pub add luthordart pub add dev:build_runner dev:luthor_generatorimport 'package:luthor/luthor.dart';
part 'user.g.dart';
@luthorclass User { const User({ @HasMin(2) required this.name, @IsEmail() required this.email, @HasMin(18) this.age, });
factory User.fromJson(Map<String, dynamic> json) => User( name: json['name'] as String, email: json['email'] as String, age: json['age'] as int?, );
final String name; final String email; final int? age;
Map<String, dynamic> toJson() => {'name': name, 'email': email, 'age': age};}A hand-written fromJson has to read only the keys of its constructor parameters, because the schema strips every other key before fromJson runs.
const UserSchemaKeys = (name: "name", email: "email", age: "age");
final SchemaValidator $UserSchema = l .schema({ UserSchemaKeys.name: l.string().min(2).required(), UserSchemaKeys.email: l.string().email().required(), UserSchemaKeys.age: l.int().min(18), }) .withName("User");dart pub add luthor dart_mappabledart pub add dev:build_runner dev:luthor_generator dev:dart_mappable_builderimport 'package:dart_mappable/dart_mappable.dart';import 'package:luthor/luthor.dart';
part 'user.g.dart';part 'user.mapper.dart';
@luthor@MappableClass(caseStyle: CaseStyle.snakeCase)class User with UserMappable { const User({ @HasMin(2) required this.fullName, @IsEmail() required this.email, @HasMin(18) this.age, });
final String fullName; final String email; final int? age;}The validate function calls UserMapper.fromMap, validateSelf calls toMap(), and caseStyle applies to the keys:
const UserSchemaKeys = (fullName: "full_name", email: "email", age: "age");
final SchemaValidator $UserSchema = l .schema({ UserSchemaKeys.fullName: l.string().min(2).required(), UserSchemaKeys.email: l.string().email().required(), UserSchemaKeys.age: l.int().min(18), }) .withName("User");
ValidationResult<User> $UserValidate(Object? json) => $UserSchema.validateSchema(json, fromJson: UserMapper.fromMap);
extension UserValidationExtension on User { ValidationResult<User> validateSelf() => $UserValidate(toMap());All three validate the same input the same way:
$UserValidate(input), for each setup+3-1- valid:
json_serializable: {'full_name': 'Ada', 'email': '[email protected]'} - valid:
plain class: {'name': 'Ada', 'email': '[email protected]'} - valid:
dart_mappable: {'full_name': 'Ada', 'email': '[email protected]'} - invalid:
plain class: {'name': 'A', 'email': 'ada'}
full_namefull_namenameWhat the serializer tells the generator
Section titled “What the serializer tells the generator”The generator reads the serializer’s annotations so the schema matches what fromJson will do:
| Annotation | Effect on the schema |
|---|---|
@JsonKey(name:), @JsonSerializable(fieldRename:) on the class or the freezed factory |
The key |
@MappableField(key:), @MappableClass(caseStyle:) |
The key |
@JsonKey(defaultValue:), a constructor default, @Default |
The field is optional |
@JsonKey(includeFromJson: false) |
The field is left out |
@JsonKey(fromJson:), a JsonConverter on the field, class or @JsonSerializable(converters:) |
The field is l.any() |
@JsonKey(readValue:) |
The field is an optional l.any() and the schema gets .passthrough() |
@JsonSerializable(createToJson: false), @Freezed(toJson: false) |
No validateSelf |
Public settable fields on a @JsonSerializable class that are not constructor parameters |
Schema fields, required unless nullable or defaulted |
Global options in another builder’s build.yaml, such as json_serializable’s field_rename, are not read. Put them on the class.
Classes you do not annotate
Section titled “Classes you do not annotate”A field whose type is a class without @luthor still gets a schema, as long as that class meets the requirements above. The generator emits a private _$ClassSchema for it in the library that uses it, without public keys or a validate function. Annotate the class with @luthor when you want those.