Skip to content

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.

  • 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 fromJson factory, generative constructor or static method, or the @MappableClass annotation from dart_mappable.
  • For validateSelf, a toJson() method or dart_mappable’s toMap(). 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.

Terminal window
dart pub add luthor json_annotation
dart pub add dev:build_runner dev:luthor_generator dev:json_serializable
lib/user.dart
import '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:

lib/user.g.dart
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");

All three validate the same input the same way:

$UserValidate(input), for each setup+3-1
  1. valid: json_serializable: {'full_name': 'Ada', 'email': '[email protected]'}
  2. valid: plain class: {'name': 'Ada', 'email': '[email protected]'}
  3. valid: dart_mappable: {'full_name': 'Ada', 'email': '[email protected]'}
  4. invalid: plain class: {'name': 'A', 'email': 'ada'}name: name must be at least 2 characters longemail: email must be a valid email address
UserSchemaKeys.fullName for json_serializable and dart_mappable, UserSchemaKeys.name for the plain class
full_name
full_name
name

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.

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.