cv models
package:cv maps Dart objects to Map<String, Object?> and back with no code
generation. A model is a class whose fields are CvField objects. The field
holds the serialized key and a mutable value. The model lists its fields once
and gets toMap(), fromMap(), ==, clone(), copyFrom() and JSON for free.
Guidelines
- Extend
CvModelBase. Declare each field as final x = CvField<T>('key');
and override CvFields get fields => [...] listing every field. A field not
in fields is never serialized.
- Read and write values through the field:
model.title.v (alias .value).
Use .valueOrThrow when a value is required, .valueOrNull when optional.
- Distinguish unset from null. An unset field is absent from
toMap(). A field
set to null with .v = null is present with a null value. setValue(null)
unsets unless presentIfNull: true. clear() unsets. hasValue is true for
present values, including null. isNull is true for both unset and null.
fromMap() merges: only keys present in the map are written, other fields
keep their value. Call clear() first, or build a new object with
map.cv<T>(), when you need a full replacement.
- Register every model once with
cvAddConstructor(MyModel.new) (or
cvAddConstructors([...])) before calling map.cv<T>(),
json.cv<T>(), cvNewModel<T>() or clone(). A missing registration throws
CvBuilderException with "Missing builder for 'T'". Group registrations in
an idempotent initXxxBuilders() function per package.
- Field types must be
String, int, double, num, bool, DateTime,
List, Map, an enum or another CvModel. Use CvListField<T> for lists
of basic types, CvModelField<T> for one nested model, CvModelListField<T>
for a list of models and CvModelMapField<T> for a Map<String, T> of
models. Nested model types need a registered constructor too.
- Numbers read from a map are coerced to the field type (
1.7 into
CvField<int> gives 2, 1 into CvField<double> gives 1.0).
- Use
CvField.encodedEnum('key', MyEnum.values) to store an enum as its
name string, and CvField.encodedDateTime('key') to store a DateTime as
an ISO 8601 string. A plain CvField<DateTime> keeps the DateTime object
in the map, which is fine for sembast or Firestore but not for JSON.
- Import
package:cv/cv_json.dart (re-exports cv.dart) to get toJson(),
toJsonPretty(), String.cv<T>() and String.cvList<T>().
- Equality (
==) and cvModelsAreEquals compare toMap() content deeply, so
two models with the same values are equal.
- Prefer
CvFields (a List<CvField>) as the type of fields.
- Put derived helpers in an
extension MyModelExt on MyModel rather than in
the class, and keep the class limited to field declarations.
Examples
Define a model and convert it
import 'package:cv/cv.dart';
class Note extends CvModelBase {
final title = CvField<String>('title');
final content = CvField<String>('content');
final date = CvField<DateTime>('date');
@override
CvFields get fields => [title, content, date];
}
void main() {
var note = Note()
..title.v = 'My note'
..content.v = 'My note context'
..date.v = DateTime(2021, 08, 16);
print(note.toMap());
// {title: My note, content: My note context, date: 2021-08-16 00:00:00.000}
note = Note()..fromMap({'title': 'Other'});
print(note.title.v); // Other
print(note.content.hasValue); // false
}
Register constructors and build from a map
void initNoteBuilders() {
cvAddConstructor(Note.new);
}
void main() {
initNoteBuilders();
var note = {'title': 'My note'}.cv<Note>();
var notes = [<String, Object?>{'title': 'a'}, {'title': 'b'}].cv<Note>();
var empty = cvNewModel<Note>();
var copy = note.clone();
}
Nested models and lists
class Point extends CvModelBase {
final x = CvField<int>('x');
final y = CvField<int>('y');
@override
CvFields get fields => [x, y];
}
class Shape extends CvModelBase {
final origin = CvModelField<Point>('origin');
final points = CvModelListField<Point>('points');
final tags = CvListField<String>('tags');
final named = CvModelMapField<Point>('named');
@override
CvFields get fields => [origin, points, tags, named];
}
void main() {
cvAddConstructors([Point.new, Shape.new]);
var shape = {
'origin': {'x': 0, 'y': 0},
'points': [
{'x': 1, 'y': 2},
{'x': 3, 'y': 4},
],
'tags': ['a', 'b'],
}.cv<Shape>();
print(shape.points.v![1].y.v); // 4
// Building nested content in code
shape.origin.v = Point()
..x.v = 10
..y.v = 20;
shape.points.v = [Point()..x.v = 1];
// Or create an empty child through the field:
var child = shape.points.create({})..x.v = 5;
shape.points.v!.add(child);
}
Enum and DateTime encoded for JSON
enum Status { draft, published }
class Article extends CvModelBase {
final status = CvField.encodedEnum('status', Status.values);
final createdAt = CvField.encodedDateTime('createdAt');
@override
CvFields get fields => [status, createdAt];
}
void main() {
cvAddConstructor(Article.new);
var article = Article()
..status.v = Status.published
..createdAt.v = DateTime.utc(2024, 1, 2);
print(article.toMap());
// {status: published, createdAt: 2024-01-02T00:00:00.000Z}
print(article.status.v); // Status.published
}
JSON
import 'package:cv/cv_json.dart';
void main() {
cvAddConstructor(Note.new);
var note = '{"title":"My note"}'.cv<Note>();
var notes = '[{"title":"a"},{"title":"b"}]'.cvList<Note>();
print(note.toJson()); // {"title":"My note"}
print(notes.toJson()); // [{"title":"a"},{"title":"b"}]
print(note.toJsonPretty());
}
Null, unset and partial maps
var note = Note();
note.toMap(); // {}
note.title.v = null;
note.toMap(); // {title: null}
note.title.setValue(null);
note.toMap(); // {}
note.toMap(includeMissingValue: true); // {title: null, content: null, date: null}
// Only export or import some columns
note.toMap(columns: ['title']);
note.fromMap(map, columns: ['title', 'content']);
note.copyFrom(otherNote, columns: ['title']);
Reuse fields through a mixin
mixin TimestampsMixin on CvModelBase {
final createdAt = CvField.encodedDateTime('createdAt');
final updatedAt = CvField.encodedDateTime('updatedAt');
CvFields get timestampFields => [createdAt, updatedAt];
}
class Post extends CvModelBase with TimestampsMixin {
final title = CvField<String>('title');
@override
CvFields get fields => [title, ...timestampFields];
}
Common mistakes
- Forgetting to add a new field to
fields. It silently stays out of the map.
- Calling
map.cv<T>() before cvAddConstructor(T.new).
- Mixing base classes in one
cvAddConstructors call. Register CvModelBase
models and CvMapModelBase models in separate calls.
- Expecting
fromMap() to reset fields absent from the map.
- Using
CvField<DateTime> in a model that is sent as JSON. Use
CvField.encodedDateTime instead.
- Declaring a field as
CvField<MyModel>. Use CvModelField<MyModel> so the
nested map is converted.
More
See references/advanced.md for CvMapModelBase
(models that keep unknown keys), raw Model map helpers, tree paths
(cvPath, cvTreeValueAtPath), withParent flattened keys, multi-type
fields, and the test helpers in package:cv/cv_matcher.dart (cvEquals,
cvEqualsDiffReport, fillModel).
1---2name: cv-models3description: Use when defining, reading, writing or serializing data models with package:cv (CvModelBase, CvField, CvModelField, CvModelListField, cvAddConstructor, toMap/fromMap, toJson/cv<T>()). Covers field declaration, null vs unset semantics, builder registration, nested models, lists, enums, DateTime encoding and JSON helpers, without code generation.4---56# cv models78`package:cv` maps Dart objects to `Map<String, Object?>` and back with no code9generation. A model is a class whose fields are `CvField` objects. The field10holds the serialized key and a mutable value. The model lists its fields once11and gets `toMap()`, `fromMap()`, `==`, `clone()`, `copyFrom()` and JSON for free.1213## Guidelines1415* Extend `CvModelBase`. Declare each field as `final x = CvField<T>('key');`16 and override `CvFields get fields => [...]` listing every field. A field not17 in `fields` is never serialized.18* Read and write values through the field: `model.title.v` (alias `.value`).19 Use `.valueOrThrow` when a value is required, `.valueOrNull` when optional.20* Distinguish unset from null. An unset field is absent from `toMap()`. A field21 set to null with `.v = null` is present with a null value. `setValue(null)`22 unsets unless `presentIfNull: true`. `clear()` unsets. `hasValue` is true for23 present values, including null. `isNull` is true for both unset and null.24* `fromMap()` merges: only keys present in the map are written, other fields25 keep their value. Call `clear()` first, or build a new object with26 `map.cv<T>()`, when you need a full replacement.27* Register every model once with `cvAddConstructor(MyModel.new)` (or28 `cvAddConstructors([...])`) before calling `map.cv<T>()`,29 `json.cv<T>()`, `cvNewModel<T>()` or `clone()`. A missing registration throws30 `CvBuilderException` with "Missing builder for 'T'". Group registrations in31 an idempotent `initXxxBuilders()` function per package.32* Field types must be `String`, `int`, `double`, `num`, `bool`, `DateTime`,33 `List`, `Map`, an enum or another `CvModel`. Use `CvListField<T>` for lists34 of basic types, `CvModelField<T>` for one nested model, `CvModelListField<T>`35 for a list of models and `CvModelMapField<T>` for a `Map<String, T>` of36 models. Nested model types need a registered constructor too.37* Numbers read from a map are coerced to the field type (`1.7` into38 `CvField<int>` gives `2`, `1` into `CvField<double>` gives `1.0`).39* Use `CvField.encodedEnum('key', MyEnum.values)` to store an enum as its40 name string, and `CvField.encodedDateTime('key')` to store a `DateTime` as41 an ISO 8601 string. A plain `CvField<DateTime>` keeps the `DateTime` object42 in the map, which is fine for sembast or Firestore but not for JSON.43* Import `package:cv/cv_json.dart` (re-exports `cv.dart`) to get `toJson()`,44 `toJsonPretty()`, `String.cv<T>()` and `String.cvList<T>()`.45* Equality (`==`) and `cvModelsAreEquals` compare `toMap()` content deeply, so46 two models with the same values are equal.47* Prefer `CvFields` (a `List<CvField>`) as the type of `fields`.48* Put derived helpers in an `extension MyModelExt on MyModel` rather than in49 the class, and keep the class limited to field declarations.5051## Examples5253### Define a model and convert it5455```dart56import 'package:cv/cv.dart';5758class Note extends CvModelBase {59 final title = CvField<String>('title');60 final content = CvField<String>('content');61 final date = CvField<DateTime>('date');6263 @override64 CvFields get fields => [title, content, date];65}6667void main() {68 var note = Note()69 ..title.v = 'My note'70 ..content.v = 'My note context'71 ..date.v = DateTime(2021, 08, 16);72 print(note.toMap());73 // {title: My note, content: My note context, date: 2021-08-16 00:00:00.000}7475 note = Note()..fromMap({'title': 'Other'});76 print(note.title.v); // Other77 print(note.content.hasValue); // false78}79```8081### Register constructors and build from a map8283```dart84void initNoteBuilders() {85 cvAddConstructor(Note.new);86}8788void main() {89 initNoteBuilders();90 var note = {'title': 'My note'}.cv<Note>();91 var notes = [<String, Object?>{'title': 'a'}, {'title': 'b'}].cv<Note>();92 var empty = cvNewModel<Note>();93 var copy = note.clone();94}95```9697### Nested models and lists9899```dart100class Point extends CvModelBase {101 final x = CvField<int>('x');102 final y = CvField<int>('y');103104 @override105 CvFields get fields => [x, y];106}107108class Shape extends CvModelBase {109 final origin = CvModelField<Point>('origin');110 final points = CvModelListField<Point>('points');111 final tags = CvListField<String>('tags');112 final named = CvModelMapField<Point>('named');113114 @override115 CvFields get fields => [origin, points, tags, named];116}117118void main() {119 cvAddConstructors([Point.new, Shape.new]);120 var shape = {121 'origin': {'x': 0, 'y': 0},122 'points': [123 {'x': 1, 'y': 2},124 {'x': 3, 'y': 4},125 ],126 'tags': ['a', 'b'],127 }.cv<Shape>();128 print(shape.points.v![1].y.v); // 4129130 // Building nested content in code131 shape.origin.v = Point()132 ..x.v = 10133 ..y.v = 20;134 shape.points.v = [Point()..x.v = 1];135 // Or create an empty child through the field:136 var child = shape.points.create({})..x.v = 5;137 shape.points.v!.add(child);138}139```140141### Enum and DateTime encoded for JSON142143```dart144enum Status { draft, published }145146class Article extends CvModelBase {147 final status = CvField.encodedEnum('status', Status.values);148 final createdAt = CvField.encodedDateTime('createdAt');149150 @override151 CvFields get fields => [status, createdAt];152}153154void main() {155 cvAddConstructor(Article.new);156 var article = Article()157 ..status.v = Status.published158 ..createdAt.v = DateTime.utc(2024, 1, 2);159 print(article.toMap());160 // {status: published, createdAt: 2024-01-02T00:00:00.000Z}161 print(article.status.v); // Status.published162}163```164165### JSON166167```dart168import 'package:cv/cv_json.dart';169170void main() {171 cvAddConstructor(Note.new);172 var note = '{"title":"My note"}'.cv<Note>();173 var notes = '[{"title":"a"},{"title":"b"}]'.cvList<Note>();174 print(note.toJson()); // {"title":"My note"}175 print(notes.toJson()); // [{"title":"a"},{"title":"b"}]176 print(note.toJsonPretty());177}178```179180### Null, unset and partial maps181182```dart183var note = Note();184note.toMap(); // {}185note.title.v = null;186note.toMap(); // {title: null}187note.title.setValue(null);188note.toMap(); // {}189note.toMap(includeMissingValue: true); // {title: null, content: null, date: null}190191// Only export or import some columns192note.toMap(columns: ['title']);193note.fromMap(map, columns: ['title', 'content']);194note.copyFrom(otherNote, columns: ['title']);195```196197### Reuse fields through a mixin198199```dart200mixin TimestampsMixin on CvModelBase {201 final createdAt = CvField.encodedDateTime('createdAt');202 final updatedAt = CvField.encodedDateTime('updatedAt');203204 CvFields get timestampFields => [createdAt, updatedAt];205}206207class Post extends CvModelBase with TimestampsMixin {208 final title = CvField<String>('title');209210 @override211 CvFields get fields => [title, ...timestampFields];212}213```214215## Common mistakes216217* Forgetting to add a new field to `fields`. It silently stays out of the map.218* Calling `map.cv<T>()` before `cvAddConstructor(T.new)`.219* Mixing base classes in one `cvAddConstructors` call. Register `CvModelBase`220 models and `CvMapModelBase` models in separate calls.221* Expecting `fromMap()` to reset fields absent from the map.222* Using `CvField<DateTime>` in a model that is sent as JSON. Use223 `CvField.encodedDateTime` instead.224* Declaring a field as `CvField<MyModel>`. Use `CvModelField<MyModel>` so the225 nested map is converted.226227## More228229See [references/advanced.md](references/advanced.md) for `CvMapModelBase`230(models that keep unknown keys), raw `Model` map helpers, tree paths231(`cvPath`, `cvTreeValueAtPath`), `withParent` flattened keys, multi-type232fields, and the test helpers in `package:cv/cv_matcher.dart` (`cvEquals`,233`cvEqualsDiffReport`, `fillModel`).