Skip to content

Commit 0c07ece

Browse files
committed
feat: add field and class level serialization hooks to exclude null values
Refs: #15
1 parent a630151 commit 0c07ece

File tree

3 files changed

+127
-13
lines changed

3 files changed

+127
-13
lines changed

packages/dogs_core/lib/src/hooks.dart

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ abstract class SerializationHook implements StructureMetadata {
4040
/// - `map`: The map representation of the object to be deserialized.
4141
/// - `structure`: The `DogStructure` instance associated with the object to be deserialized.
4242
/// - `engine`: The `DogEngine` instance that will perform the deserialization.
43-
void beforeDeserialization(Map<String, dynamic> map, DogStructure structure,
44-
DogEngine engine) {}
43+
void beforeDeserialization(
44+
Map<String, dynamic> map, DogStructure structure, DogEngine engine) {}
4545
}
4646

4747
/// `FieldSerializationHook` is a mixin for field structure metadata
@@ -50,12 +50,19 @@ abstract class SerializationHook implements StructureMetadata {
5050
///
5151
/// This interface is used by [NativeSerializerMode] and descendants.
5252
mixin FieldSerializationHook on StructureMetadata {
53-
5453
/// This method is called after a field is serialized by [NativeSerializerMode].
55-
void postFieldSerialization(NativeStructureContext context, NativeStructureFieldContext fieldContext, Map<String, dynamic> map, DogEngine engine) {}
54+
void postFieldSerialization(
55+
NativeStructureContext context,
56+
NativeStructureFieldContext fieldContext,
57+
Map<String, dynamic> map,
58+
DogEngine engine) {}
5659

5760
/// This method is called before a field is deserialized by [NativeSerializerMode].
58-
void beforeFieldDeserialization(NativeStructureContext context, NativeStructureFieldContext fieldContext, Map<String, dynamic> map, DogEngine engine) {}
61+
void beforeFieldDeserialization(
62+
NativeStructureContext context,
63+
NativeStructureFieldContext fieldContext,
64+
Map<String, dynamic> map,
65+
DogEngine engine) {}
5966
}
6067

6168
/// A function that may be used to transform a map before deserialization.
@@ -79,8 +86,8 @@ class LightweightMigration extends SerializationHook {
7986

8087
/// Executes each migration function in the `migrations` list before deserialization.
8188
@override
82-
void beforeDeserialization(Map<String, dynamic> map, DogStructure structure,
83-
DogEngine engine) {
89+
void beforeDeserialization(
90+
Map<String, dynamic> map, DogStructure structure, DogEngine engine) {
8491
for (var value in migrations) {
8592
value(map, structure, engine);
8693
}
@@ -112,8 +119,8 @@ class RevisionMigration extends SerializationHook {
112119
/// Executes each migration function in the `migrations` list that corresponds to
113120
/// a version number greater than or equal to the current version.
114121
@override
115-
void beforeDeserialization(Map<String, dynamic> map, DogStructure structure,
116-
DogEngine engine) {
122+
void beforeDeserialization(
123+
Map<String, dynamic> map, DogStructure structure, DogEngine engine) {
117124
final version = map[revisionKey] as int? ?? 0;
118125
for (var i = version; i < migrations.length; i++) {
119126
if (i >= version) {
@@ -146,26 +153,64 @@ class DefaultValue extends StructureMetadata with FieldSerializationHook {
146153
/// otherwise it will be removed if the field is equal to the default value.
147154
const DefaultValue(this.value, {this.keep = false});
148155

149-
dynamic _getNativeDefault(NativeStructureFieldContext fieldContext, DogEngine engine) {
156+
dynamic _getNativeDefault(
157+
NativeStructureFieldContext fieldContext, DogEngine engine) {
150158
final provided = value is DefaultValueSupplier ? value() : value;
151159
final native = fieldContext.encodeValue(provided, engine);
152160
return native;
153161
}
154162

155163
@override
156-
void beforeFieldDeserialization(NativeStructureContext context, NativeStructureFieldContext fieldContext, Map<String, dynamic> map, DogEngine engine) {
164+
void beforeFieldDeserialization(
165+
NativeStructureContext context,
166+
NativeStructureFieldContext fieldContext,
167+
Map<String, dynamic> map,
168+
DogEngine engine) {
157169
if (!map.containsKey(fieldContext.key)) {
158170
map[fieldContext.key] = _getNativeDefault(fieldContext, engine);
159171
}
160172
}
161173

162174
@override
163-
void postFieldSerialization(NativeStructureContext context, NativeStructureFieldContext fieldContext, Map<String, dynamic> map, DogEngine engine) {
175+
void postFieldSerialization(
176+
NativeStructureContext context,
177+
NativeStructureFieldContext fieldContext,
178+
Map<String, dynamic> map,
179+
DogEngine engine) {
164180
if (!keep) {
165181
final nativeValue = _getNativeDefault(fieldContext, engine);
166182
if (deepEquality.equals(map[fieldContext.key], nativeValue)) {
167183
map.remove(fieldContext.key);
168184
}
169185
}
170186
}
171-
}
187+
}
188+
189+
/// A **field** and **class** level serialization hook that excludes fields
190+
/// with a `null` value from the serialized map.
191+
const ExcludeNull excludeNull = ExcludeNull();
192+
193+
/// A **field** and **class** level serialization hook that excludes fields
194+
/// with a `null` value from the serialized map.
195+
class ExcludeNull extends SerializationHook with FieldSerializationHook {
196+
const ExcludeNull();
197+
198+
@override
199+
void postFieldSerialization(
200+
NativeStructureContext context,
201+
NativeStructureFieldContext fieldContext,
202+
Map<String, dynamic> map,
203+
DogEngine engine) {
204+
// Remove the field if its value is null
205+
if (map[fieldContext.key] == null) {
206+
map.remove(fieldContext.key);
207+
}
208+
}
209+
210+
@override
211+
void postSerialization(dynamic obj, Map<String, dynamic> map,
212+
DogStructure structure, DogEngine engine) {
213+
// Remove all null values from the map
214+
map.removeWhere((key, value) => value == null);
215+
}
216+
}

smoke/test0/lib/parts/models.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ void testModels() {
3636
testSingleModel<GetterModel>(GetterModel.variant0, GetterModel.variant1);
3737
testSingleModel<DefaultValueModel>(
3838
DefaultValueModel.variant0, DefaultValueModel.variant1);
39+
testSingleModel<FieldExclusionModel>(
40+
FieldExclusionModel.variant0, FieldExclusionModel.variant1);
41+
testSingleModel<ClassExclusionModel>(
42+
ClassExclusionModel.variant0, ClassExclusionModel.variant1);
43+
3944

4045
test("Default Values", () {
4146
var defaultValues = DefaultValueModel.variant0();
@@ -55,6 +60,17 @@ void testModels() {
5560
expect(serialName, isNotNull);
5661
expect(serialName, "MyCustomSerialName");
5762
});
63+
64+
test("Exclusion Hooks", () {
65+
final fieldMap = dogs.toNative(FieldExclusionModel.variant1()) as Map;
66+
final classMap = dogs.toNative(ClassExclusionModel.variant1()) as Map;
67+
68+
expect(fieldMap.containsKey("always"), true);
69+
expect(fieldMap.containsKey("maybe"), false);
70+
71+
expect(classMap.containsKey("a"), false);
72+
expect(classMap.containsKey("b"), false);
73+
});
5874
}
5975

6076
void testSingleModel<T>(T Function() a, T Function() b) => group("$T", () {

smoke/test0/lib/special.dart

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,4 +196,57 @@ class DefaultValueModel with Dataclass<DefaultValueModel> {
196196
class CustomSerialName {
197197
String value;
198198
CustomSerialName(this.value);
199+
}
200+
201+
@serializable
202+
class FieldExclusionModel with Dataclass<FieldExclusionModel> {
203+
String? always;
204+
205+
@excludeNull
206+
String? maybe;
207+
208+
FieldExclusionModel({
209+
this.always,
210+
this.maybe,
211+
});
212+
213+
factory FieldExclusionModel.variant0() {
214+
return FieldExclusionModel(
215+
always: "always",
216+
maybe: "maybe",
217+
);
218+
}
219+
220+
factory FieldExclusionModel.variant1() {
221+
return FieldExclusionModel(
222+
always: null,
223+
maybe: null,
224+
);
225+
}
226+
}
227+
228+
@serializable
229+
@excludeNull
230+
class ClassExclusionModel with Dataclass<ClassExclusionModel> {
231+
String? a;
232+
String? b;
233+
234+
ClassExclusionModel({
235+
this.a,
236+
this.b,
237+
});
238+
239+
factory ClassExclusionModel.variant0() {
240+
return ClassExclusionModel(
241+
a: "always",
242+
b: "maybe",
243+
);
244+
}
245+
246+
factory ClassExclusionModel.variant1() {
247+
return ClassExclusionModel(
248+
a: null,
249+
b: null,
250+
);
251+
}
199252
}

0 commit comments

Comments
 (0)