Moor: toJson returns a Map instead of a jsonEncoded String

Created on 7 May 2019  路  10Comments  路  Source: simolus3/moor

Similarly, fromJson function takes a Map, instead of a JSON string:

Map<String, dynamic> toJson()
factory Xxx.fromJson(Map<String, dynamic> json)

How can I convert these functions to standard:

String toJson() => jsonEncode(toMap())
factory Xxx.fromJson(String json) => Xxx.fromMap(jsonDecode(json));

All 10 comments

As this library generates your data classes, you can't modify them. Instead, you should put code that deals with them elsewhere. You might want to put the serialization methods in the table class:

```dart
import 'dart:convert';
import 'package:moor_flutter/moor_flutter.dart';

class Todos extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get title => text().withLength(min: 6, max: 10)();
TextColumn get content => text().named('body')();
IntColumn get category => integer().nullable()();

static String mapEntryToJson(Todo entry) {
return json.encode(entry.toJson());
}

static Todo fromJson(String encodedJson) {
return Todo.fromJson(json.decode(encodedJson) as Map);
}
}```

returning a String should be a build in feature because jsonEncode can't convert everything e.g. DateTime

I've thought about this for a while, but I'm still not convinced that this feature should be addressed in this library:

  1. The library cannot know how to encode datetimes for your use case - should it be a unix timestamp, some ISO 8601 format, or a localized human-readable notation? By just outputting a string, the library would have to make that decision for you, which might not be what you want. By outputting a map however, you can provide the toEncodable parameter on json.encode and make that decision yourself.
  2. I'm not a big fan of serialization in this library. Serializing rows almost always implies that you're trying to do something this library was not designed to do. The goal is to provide a typesafe, reactive layer for local persistence. The generated classes are not designed to be used for network communication or business logic - they're just wrapper objects around some row in a table.

As a solution to this problem, I recommend not using the generated classes as model classes in your application. Instead, you can write your own model/data classes, implement serialization or business logic in them, and then write a mapper to convert these from and to moor row classes. A rough example:

// your model class. You can implement this however you want: It can have methods for business
// logic, be serialize (for instance with built_value) etc. This is also the class that you would pass
// around in your application, by providing it to widgets and so on.
class TodoEntry {
  final String content;
  final DateTime dueTo;
  // implement constructor etc.
}

// The implementation of this class will be responsible for locally storing todo entries, so you don't
// have to worry about moor anywhere else in your application.
abstract class TodoRepository {
  Future<List<TodoEntry>> loadDueBefore(DateTime due);
}

// finally, you can implement them using moor
class Todos extends Table {
  // ...
  DateTimeColumn get dueBefore => dateTime()();
}

@UseMoor(tables: [Todos])
class MyDatabase extends _$MyDatabase implements TodoRepository {
  // ...

  @override
  Future<TodoEntry> loadDueBefore(DateTime due) async {
    final results = await (select(todos)..where((t) => t.dueBefore.isBefore(due))).get();
    return results.map((row) {
      return new TodoEntry(/*...*/);
    }).toList();
  }
}

Now, you don't have to worry about the table classes not being suitable for you, as they only do what they've been designed to do: Representing a single row in a database. It also makes your code less tightly coupled as persistence objects are not being used outside of code that directly deals with persistence.
I've used this approach with room and sqldelight before and it worked out well for me. Perhaps the documentation should be clearer about the generated classes not being suitable for serialization. Also, the example doesn't really respect this, so I might need to address that as well.

I am working on a project which has a local database, and also an online backend database. And these two of course have to be kept in sync. Also this is an ideal use case, so you can't ignore one or the other. Originally, I was not using this library, and had separate parts for both local and online. But the biggest reason to actually move was a lot more maintainable and contained code, for the above mentioned use case. So, I hope you will understand that asking for two separate parts will be alienating a big use case.

Secondly, I believe in sensible defaults out of the box, with an option to override them. It will not be hard to implement an interface (I also created an issue on this), which can implement (de)serialization (to/from a String). And if a person doesn't was it, then he can simply opt-out of that interface.

That's a fair point! I'll try to get that improved serialization ready in the next major version, but I can't make any promises on when that will be ready.

The DataClass, from which all generated classes inherit, now has toJson and toJsonString. The serialization behavior can be customized by passing a ValueSerializer. An example can be found in the tests: https://github.com/simolus3/moor/blob/ce2e6afc6fdba806df0603b5b27058ee0f7048fc/moor/test/serialization_test.dart#L32-L54
Further, the fromJson constructor also accepts a custom serializer. I'll release 1.4, which contains this feature, later today or tomorrow.

Great Work! I was already following your work, and already using the development branch. One suggestion before you push, will be to add a corresponding fromJsonString method too.

PS: you should bump the major version to 2.0, you pushed a breaking change earlier too without bumping the major version (it broke the build on "packages upgrade")

Regarding the fromJsonString constructor: The library would have to generate another constructor for each data class in that case. That might be unwanted for some, so it's an opt-in for now. You can configure the builder by creating a file called build.yaml in your project with the following content

targets:
  $default:
    builders:
      moor_generator:
        options:
          write_from_json_string_constructor: true

(I'll definitely write public documentation for that soon)

For the version numbers: I think it's typical for Dart packages to only bump the minor version when a minor breaking change occurs (both for flutter and most packages from the Dart team). Can you tell me what version change of moor broke your builds, and why? Pub recommends using a format like moor: ^1.3.0, which would not upgrade to an 1.4 version.

Great!

Having major version 0 means your API is unstable, then you can make breaking changes just by incrementing the minor version. But in your case the major version is already 1, which means API is stable. So, now you have to update the major version, whenever you make a breaking change.
moor: ^1.3.0 will upgrade to any version less than 2.0.0

The update which broke build was from version 1.1.0 to 1.2.0 (in which the name of daos changed)

See this: https://stackoverflow.com/a/53563080

Ah thanks for the pointers, I wasn't aware that the behavior for 0.x versions is different from other versions, that makes sense.

I just reviewed the diff from development to master and couldn't find a breaking change though - the custom serializer is an optional parameter everywhere it's used. The other features (changing the json key and the new real colmn) are optional as well.

Was this page helpful?
0 / 5 - 0 ratings