Git Product home page Git Product logo

dart_serialize_proposal's Introduction

Dart for Serialization (Proposal)

Design and open discussion around better Dart serialization experiences.

This is not an official Google or Dart design or process is primarily to experiment and collect feedback from both internal and external stakeholders and users. There is a chance this might result in no changes, or changes could occur at some future point in time.

Please read CONTRIBUTING.md before opening a PR

Table of contents

Background

Dart is toted as a modern object-oriented client-side programming language (with some but more limited usage in standalone tools and servers), but lacks a well defined story for serialization - something that is paramount to adoption and healthy developer ergonomics.

With the introduction of Flutter, Dart users have the ability to produce high-fidelity iOS and Android applications, but have also run into the issue of having a typed object model that serializes to/from data formats like JSON.

A prior review of options in Dart that is a bit of out date is available on the Dart website as an article: "Serialization in Dart". It's a good place to start if you don't know much about the subject.

Available Options

Not surprisingly, users are effectively using Dart with JSON and other formats like protocol buffers, today, both internally and externally, but often with some sort of downside.

Use untyped data structures

The simplest option - just don't use types. This often produces the lowest overhead in terms of both runtime and code-size, and can even scale OK for small projects or in cases where the serialization format is fluid.

import 'dart:async';
import 'dart:convert';

import 'package:http/http.dart' as http;

Future<Map<String, dynamic>>> fetchAccount(int id) async {
  var response = await http.get('account/$id');
  return JSON.decode(response.body) as Map<String, dynamic>;
}
main() async {
  var account = await fetchAccount(101);
  print('Account #101 is for: ${account['name']'});
}

PROs

  • Batteries included: you rarely have to go beyond the core libraries.
  • Works equally well in all platforms (browser/server/mobile).
  • Produces the lowest overhead in terms of both code-size and runtime.

CONs

  • Toolability: Good luck renaming account['name'] to account['first_name'].
  • Can't statically validate against a schema or format.
  • Doesn't scale well to large teams.
  • Exposes a mutable and iterable data structure for everything.

Who is using this approach

  • Small teams or single developers/prototypers.
  • Applications with unstructured data (so Map or List is actually OK).

Use runtime reflection

Dart's runtime reflection library (dart:mirrors) can read from and write to structured classes at runtime, and use metadata like annotations for developers to be able to add extra information.

import 'dart:convert';
import 'dart:mirrors';

class Account {
  final int id;

  Account({this.id});
}

class Serializer<T> {
  const Serializer<T>();

  T decode(String json) {
    var type = reflectType(T);
    var constructor = type.constructors.first;
    var parameters = constructor.namedParameters;
    return type.newInstance(
      namedParameters: JSON.decode(json),
    );
  }
}

main() {
  var account = const Serializer<Account>().decode(r'''
    {
      "id": 101
    }
  ''');
  print('Account #101 is for: ${account.id'});
}

PROs

  • Batteries mostly included: trivial to write a simple serialization library.
  • Potential to remove mirrors usage using source code transformation.

CONs

  • Arbitrary requirements on classes (public constructor, etc).
  • Serious platform issues: disabled for Flutter, unsuable code size on the web.
  • Runtime performance suffers compared to statically typed code.
  • Difficult to debug: reflection-based systems harder to reason about.
  • Source code transformation is mostly terrible for good build systems.

Who is using this approach

Hand written classes

Of course, hand-written code and classes can do precisely what you want. For small projects or systems that don't change often (or for precisely optmizing for your business requirements) this might make the most sense:

import 'dart:convert';

class Account {
  final int id;

  Account({this.id});
}

class AccountSerializer {
  const AccountSerializer();

  Account decode(String json) {
    var map = JSON.decode(json) as Map<String, dynamic>;
    return new Account(id: map['id']);
  }
}

main() {
  var account = const AccountSerializer().decode(r'''
    {
      "id": 101
    }
  ''');
  print('Account #101 is for: ${account.id'});
}

PROs

  • You get exactly the behavior and code you want.

CONs

  • Any medium-sized+ data model is going to be time consuming/error prone.
  • As the data model changes must change both class and serializer.
  • Hard to ever support other data formats with writing even more code.
  • Dart feels like it has platform/language issues to those who read the code.

Who is using this approach

  • Too many to count :)

Use code generation

A time-tested option, simply generate Dart code either ahead-of-time or during the development process from another data source, such as a schema, configuration file, or even Dart source code (static source analysis).

Most internal users at Google use this strategy (in some form or another, though the most common is protocol buffers) - but this also relies on fact we have bazel as a standard build system.

import 'package:my_json_generator/my_json_generator.dart';

part 'main.g.dart';

class Account {
  final int id;

  Account({this.id});
}

@generate
abstract class AccountSerializer {
  const factory AccountSerializer() = AccountSerializer$Generated;

  Account decode(String json);
}

main() {
  var account = const AccountSerializer).decode(r'''
    {
      "id": 101
    }
  ''');
  print('Account #101 is for: ${account.id'});
}

PROs

  • Nicer ergonomics compared to hand writing (once build system in place).
  • Possible to get very close to (depending on requirements) hand-written code.
  • Getting more popular in web community with introduction of CLIs.

CONs

  • Difficulty of designing the "perfect system" (definition varies).
  • Static analysis errors: until you generate main.g.dart, at least.
  • Dart lacks a complete standard build system that works equally well for all.
  • For frameworks like Flutter that are "batteries included", this falls short.

Who is using this approach

Use javascript interop

A recent alternative, made possible with the javascript interop library, is to use anonymous javascript objects which can be strongly typed and work well with code completion.

It requires direct access to the native javascript serialization functions

@JS()
library serialise.interop;

import 'package:js/js.dart';

@JS('JSON.parse')
external dynamic fromJson(String text);

@JS('JSON.stringify')
external String toJson(dynamic object);
@JS()
@anonymous
class Account {
  external int get id;
  external set id(int value);

  external factory Simple({ int id });
}
main() {
  Account account = fromJson(r'''
    {
      "id": 101
    }
  ''');
  print('Account #101 is for: ${account.id'});
}

Examples and a performance comparison

PROs

  • No reliance on mirrors or source transformation.
  • Compiles to less javascript than a hand-written class.
  • Very fast since it uses native browser functions.

CONs

  • Only works in the browser and therefore only applicable to client-side applications.
  • The interop package only supports properties and not fields so the class definition is a little more verbose.

Who is using this approach

  • Small teams or single developers/prototypers.

Problems

Ergonomics

Extensibility

Performance

Stakeholders

Who (entities or individuals) are effected or have a business need in this proposal. Note that being a user of serialization is likely not enough to be considered a stakeholder - though the goal of this document is to solicit feedback from indidiual users.

NOTE: The following list is entirely assumptive at this moment:

Dart language team

Requirements

TBD

Dart platform team

Requirements

TBD

Dart web users

Requirements

TBD

Flutter users

Requirements

TBD

References

NOTE: Languages with dynamic typing without any form of static analysis (i.e. JavaScript, Ruby, Python) are excluded - in-that often the platform's built-in serialization is enough to avoid needing anything else.

It's also not that interesting to compare against - and optimizers like Google's closure often have different requirements than the language itself.

TypeScript

TypeScript has a structural type system that is easy to overlay ontop of (unstructed) formats like JSON. As an example we can pretend that a received JSON blob representing a User has static types:

interface User {
  name:    string;
  age:     number;
  created: Date;
}

fetchById(id: int): Promise<User> {
  return http.get('/users/${id}').map((response) => JSON.parse(response));
}

function run() {
  fetchById(101).then((user) => {
    console.log('Hello! ${user.name} is ${user.age} year(s) old.');
  });
}

If decorators are introduced (see proposal) then a lightweight macro-like syntax will exist as well to generate boilerplate. As an example:

@serializable()
class User {
    constructor(name: string) {
      this._name = name;
    }

    private _name: string;

    @serialize()
    get name() {
      return this._name;
    }
}

function run() {
  const p = new Person('André'); 
  console.log(JSON.stringify(p));
}

Swift

Using the Gloss library you get some helper functions and syntax, but nothing too magical.

import Gloss

struct RepoOwner: Decodable {

    let ownerId: Int?
    let username: String?

    // MARK: - Deserialization

    init?(json: JSON) {
        self.ownerId = "id" <~~ json
        self.username = "login" <~~ json
    }
}

Or for translating to JSON:

import Gloss

struct RepoOwner: Glossy {

    let ownerId: Int?
    let username: String?

    // MARK: - Deserialization
    // ...

    // MARK: - Serialization

    func toJSON() -> JSON? {
        return jsonify([
            "id" ~~> self.ownerId,
            "login" ~~> self.username
        ])
    }
}

Kotlin

An example of the Kotson library:

import com.github.salomonbrys.kotson.*

val gson = GsonBuilder().registerTypeAdapter<Person>(personSerializer).create()
import com.github.salomonbrys.kotson.*

val gson = Gson()

// java: List<User> list = gson.fromJson(src, new TypeToken<List<User>>(){}.getType());
val list1 = gson.fromJson<List<User>>(jsonString)
val list2 = gson.fromJson<List<User>>(jsonElement)
val list3 = gson.fromJson<List<User>>(jsonReader)
val list4 = gson.fromJson<List<User>>(reader)

C#

An example of the ServiceStack library:

(You can try this example live in your browser)

using System.Linq;
using ServiceStack;
using ServiceStack.Text;

public class GithubRepository
{
    public string Name { get; set; }
    public string Description { get; set; }
    public string Url { get; set; }
    public string Homepage { get; set; }
    public string Language { get; set; }
    public int Watchers { get; set; }
    public int Forks { get; set; }
    
    public override string ToString() => Name;
}

var orgName = "ServiceStack";

var orgRepos = $"https://api.github.com/orgs/{orgName}/repos"
    .GetJsonFromUrl(httpReq => httpReq.UserAgent = "Gistlyn")
    .FromJson<GithubRepository[]>()
    .OrderByDescending(x => x.Watchers)
    .Take(5)
    .ToList();

"Top 5 {0} Github Repositories:".Print(orgName);
orgRepos.PrintDump();

// Save a copy of this *public* Gist by clicking the "Save As" below 

Rust

TODO.

Go

Go comes with the encoding/json package in the standard library which allows serialization and deserialization of data types.

Here's an example:

(you can try this example live in your browser)

package main

import (
    "encoding/json"
    "fmt"
)

const personJSON = `{
  "Name" : "John Doe",
  "Age" : 50
}`

func main() {
    // deserialize a JSON string to a 'person' type
    deserializedPerson := deserializePerson(personJSON)
    fmt.Printf("Deserialized: %+v\n\n", deserializedPerson)
    
    // serialize a 'person' type to a JSON string
    serializedPerson := serializePerson(deserializedPerson)
    fmt.Printf("Serialized: %s", serializedPerson)

}

type person struct {
    Name string
    Age  int
}

func deserializePerson(s string) person {
    var p person
    json.Unmarshal([]byte(s), &p)
    return p
}

func serializePerson(p person) string {
    b, _ := json.Marshal(p)
    return string(b)
}

Solutions

Below are partial, theoritical, and non-exhaustive potential solutions to help make Dart's serialization and JSON story better. Nothing has been agreed on - and we're open to other ideas. Likely the "solution" will involve many things

  • not just one.

Language

Allow invoking a constructor using JSON/Map

Users write code like this:

class User {
  final int id;

  User({this.id});
}

And can invoke User.new (and named parameters using a JSON/Map):

main() {
  new User(~{'id': 5});
}

Add anonymous classes

While this doesn't strictly fix the JSON/serialization issues, it does make an abstract class/interface be a more implicitly useful data model. For example:

main() {
  var user = new User {id: 5};
}

Add a new struct datatype with a different type system

struct User {
  int id;
}

And perhaps it could participate in a structural type system where something like a Map or JsObject (for web users) could be "cast" (represented as) this struct:

main() {
  var json = {id: 5} as User;
  print(json.id);
}

Add a new Json union type

typedef Json = String | num | List<Json> | Map<Json, Json> | Null;

Add macros

TODO

Invest in better dart:mirrors

TODO

Platform

I.e. something that can be better optimized/tree-shaken across platforms.

Invest in an universal build system for Dart packages

A seamless code generation story, perhaps with better analyzer integration, incremental builds, and IDE-awareness would allow the least amount of changes to the language. We would likely still need a canonical serialization library.

Allow dart2js to optimize away "wrapper" code

TODO

Packages

Provde a canonical serialization library on top of code generation

TODO

dart_serialize_proposal's People

Contributors

dantup avatar kulshekhar avatar matanlurey avatar natebosch avatar parnham avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

dart_serialize_proposal's Issues

Feedback: structs + getters

I would like to see structs with getters.

struct User {
  int id;
  String firstName;
  String lastName;

  String get fullName => "$firstName $lastName";
}

main(){
  final user = User{1, "Luke", "Pighetti"};

  print(user.fullName); /// "Luke Pighetti"
}

My thoughts on JSON in Dart

I don't know if I can add anything that isn't already described in the README or #6 but since this repo is soliciting feedback I thought I'd dump my thoughts here.

The company I work for evaluated Dart seriously a few years back and there were a bunch of things that really put us off, one of which was how difficult it seemed to do something really basic - deserialise some JSON into a class. If you consider that Dart advertises itself as "batteries included" and pitches itself as a language for building web apps it seems crazy that something as simple as JSON is so complicated (and seemingly for technical reasons? minification?).

All the existing solutions are, IMO, bad:

  • Hand-rolling is tedious and error-prone
  • Mirrors bloat code (apparently; I've never actually tried to use them, I've just seen complaints about bloated JS and bad performance)
  • Transformers are clunky / going away and hard to debug
  • Using maps loses all type safety (and one of the reasons for picking Dart over plain JS is for types!)

I'm really not sure a perfect solution can be achieved without some support from the language/platform, but there are some solutions that might be tolerable:

Fix mirrors/runtime reflection to not suck. I'm not sure why mirrors (or something similar) can't be a good solution; having classes marked with some attribute then emitting some metadata about the mappings of unminified-to-minified names doesn't seem like a huge overhead. I recently wrote some C# reflection code using Bridge.net and it transpiled to JavaScript and worked perfectly, and the metadata included was not all that bad and the performance was excellent.

Code-gen. The built_value library sounded pretty promising to me, but there were some frustrations when I tried to use it:

  1. It required a lot of "noise" in my class definitions
  2. It required everything to be immutable (this further added to the boilerplate and noise) - I am a fan of immutability but sometimes it's not needed and this seemed to really complicate things for trivial cases
  3. Even using a "watch" script it only updates when I hit save in my IDE, so I'm frequently staring at a bunch of red squiggles even after I've fixed the issue just because the generated files are out of date

If you really don't want to built anything into the platform for serialisation then I think code-gen is the best option; but in order for that to work well I think the analyzer needs support to be able to generate code without the need to save files (it already knows the contents of all the buffers). I understand this is a change from how things like built_value currently work (watching file system changes or just reading the files in a one-off script) but without it I think the dev experience is going to suck!

Whatever the solution, it needs to not require third party libraries. JSON support in a web app is too fundamental for devs to have to worry about libraries being abandoned, not tested properly, making breaking changes on a whim, etc. etc. For something so basic we really need something tested/maintained/supported by the Dart team.

Expand section on code generation - generate from Dart vs generate from interface description

For serialization where the messages are strictly values (think a typed version of the Map approach) and don't have behavior associated with them there is a choice of describing the structure with Dart code as shown, or describing the structure completely outside of Dart and generating the classes entirely.

The latter is the approach I'm using here where I start with a yaml description of the interfaces.

Feedback: approaches I use to handle deserialization

I target the web. JSON deserialization is something we do every pageload, and work with almost daily.

We employ a number of different approaches based on the project and it's needs:

  1. not using types (Map<String, dynamic> is good enough sometimes)
  2. hand-written model classes with a factory constructor (pain to maintain)
  3. we experiment with code generation (we experiment transpiling Java objects, which are to be serialized, directly to Dart)

What we considered and never adopted were:

  • a mirror based solution (too much hassle to maintain code size and still could not get it to acceptable size)
  • dartson: could be decent with code generation

In my opinion, JSON serialization and deserialization from and to typed Dart objects, should be something the platform supports and I don't need to rely on 3rd party tools to achieve it.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.