Repository: google/built_value.dart (dart)

Issue:
Serializers.deserializeWith<T> should take Object? instead of Object
TL;DR: `Serializers.deserializeWith<T>`'s requirement for a non-null `Object` argument introduces a change in runtime behavior and means that callers must manually do additional validation for correctness.

----

built_value 8.0.0-nullsafety.0 declares `Serializers.deserializeWith<T>` as:

```dart
T deserializeWith<T>(Serializer<T> serializer, Object serialized);
```

I think that the `serialized` parameter would be better as `Object?` instead of as `Object`.  The non-nullable signature prevents direct use with `json.decode`.  For example, https://medium.com/flutter/some-options-for-deserializing-json-with-flutter-7481325a4450, recommends:

```dart
final myObject = serializers.deserializeWith(
    SimpleObject.serializer, json.decode(aJsonString));
```

`json.decode` returns type `dynamic`, and it's sensible and legal for `json.decode('null')` to return `null`.  People with `implicit-casts: false` will get a (somewhat confusing) analysis error about the type mismatch:

> error • A value of type 'dynamic' can't be assigned to a variable of type 'Object' at ... • (invalid_assignment)

People might attempt to fix the error by using `json.decode(...) as Object` (as weird as that looks), or people who realize that the error is caused by a nullable/non-nullable mismatch might try `json.decode(...)!`.  However, both choices are wrong because both will fail at runtime with a `CastError`, which is different from the `DeserializationError` or `FormatException` failures that existing code expects for malformed or unexpected input.

Pull request:
Fix #964

Patch:
diff --git a/built_value/lib/serializer.dart b/built_value/lib/serializer.dart
index 859f8df8..eaea750a 100644
--- a/built_value/lib/serializer.dart
+++ b/built_value/lib/serializer.dart
@@ -123,6 +123,12 @@ abstract class Serializers {
   /// calling [serialize] with a `specifiedType`.
   Object serializeWith<T>(Serializer<T> serializer, T object);
 
+  /// Convenience method for when you want a JSON string and know the type
+  /// you're serializing. Specify the type by specifying its [Serializer]
+  /// class. Equivalent to calling [serialize] with a `specifiedType` then
+  /// calling `json.encode`.
+  String toJson<T>(Serializer<T> serializer, T object);
+
   /// Deserializes [serialized].
   ///
   /// A [Serializer] must have been provided for every type the object uses.
@@ -137,6 +143,12 @@ abstract class Serializers {
   /// calling [deserialize] with a `specifiedType`.
   T deserializeWith<T>(Serializer<T> serializer, Object serialized);
 
+  /// Convenience method for when you have a JSON string and know the type
+  /// you're deserializing. Specify the type by specifying its [Serializer]
+  /// class. Equivalent to calling [deserialize] with a `specifiedType` then
+  /// calling `json.decode`.
+  T fromJson<T>(Serializer<T> serializer, String serialized);
+
   /// Gets a serializer; returns `null` if none is found. For use in plugins
   /// and other extension code.
   Serializer? serializerForType(Type type);
diff --git a/built_value/lib/src/built_json_serializers.dart b/built_value/lib/src/built_json_serializers.dart
index a42c59c8..2990f70b 100644
--- a/built_value/lib/src/built_json_serializers.dart
+++ b/built_value/lib/src/built_json_serializers.dart
@@ -2,6 +2,8 @@
 // All rights reserved. Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+import 'dart:convert';
+
 import 'package:built_collection/built_collection.dart';
 import 'package:built_value/serializer.dart';
 
@@ -41,11 +43,21 @@ class BuiltJsonSerializers implements Serializers {
         specifiedType: FullType(serializer.types.first)) as T;
   }
 
+  @override
+  T fromJson<T>(Serializer<T> serializer, String serialized) {
+    return deserializeWith<T>(serializer, json.decode(serialized));
+  }
+
   @override
   Object serializeWith<T>(Serializer<T> serializer, T object) {
     return serialize(object, specifiedType: FullType(serializer.types.first));
   }
 
+  @override
+  String toJson<T>(Serializer<T> serializer, T object) {
+    return json.encode(serializeWith<T>(serializer, object));
+  }
+
   @override
   Object serialize(Object? object,
       {FullType specifiedType = FullType.unspecified}) {


Test patch:
diff --git a/built_value/test/serializers_test.dart b/built_value/test/serializers_test.dart
index 2ea64bb0..e5251359 100644
--- a/built_value/test/serializers_test.dart
+++ b/built_value/test/serializers_test.dart
@@ -3,6 +3,7 @@
 // license that can be found in the LICENSE file.
 
 import 'package:built_value/serializer.dart';
+import 'package:built_value/src/date_time_serializer.dart';
 import 'package:built_value/src/int_serializer.dart';
 import 'package:test/test.dart';
 
@@ -39,6 +40,16 @@ void main() {
       expect(mergedSerializers.builderFactories.keys,
           contains(FullType(TestSerializer)));
     });
+
+    test('provides convenience toJson method', () {
+      expect(serializers.toJson(DateTimeSerializer(), DateTime.utc(2020, 1, 1)),
+          '1577836800000000');
+    });
+
+    test('provides convenience fromJson method', () {
+      expect(serializers.fromJson(DateTimeSerializer(), '1577836800000000'),
+          DateTime.utc(2020, 1, 1));
+    });
   });
 }