Skip to content

Easy typesafe JSON parsing with invoke function and variadic generics #4267

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
gaaclarke opened this issue Feb 14, 2025 · 6 comments
Open
Labels
feature Proposed language feature that solves one or more problems

Comments

@gaaclarke
Copy link

Description

Users want type-safe json deserialization. Today they have to use build_runner which complicates and slows down the build process or use mirrors which isn't supported everywhere. If Object had a method that allowed people to generate Invocation objects and send them to be dispatched one could write json deserializers that don't require code generation or runtime reflection.

We already have noSuchMethod so we are halfway to having full dynamic invocation.

Simple example

class Foo {
  int? a;
}

const String _json = '''
{
  "a": 1234,
}
''';

void main() {
  test() {
    Foo foo = parse<Foo>(_json);
    expect(foo.a, 1234);
  }
}

T parse<T>(String json) {
  final jsonData = jsonDecode(json);
  
  // A null target means send it to the class.
  var result = invoke<T>(method: '(init)', target: null, args: []);
  for (var entry in jsonData.entries) {
    invoke(method: entry.key, target: result, args: [entry.value], isSetter: true);
  }
}

Nested types

The invocation above works fine for flat types. It gets more complicated for nested types though since parse only has access to 1 type parameter. In order to support that we need better support for meta-programming inside of generics. It could look something like this:

dynamic parse<NestedTypes...>(String json) {
  final jsonData = jsonDecode(json);
  return inflate<NestedTypes...>(jsonData);
}

dynamic inflate<NestedTypes...>(dynamic data) {
  return data is Map && data.hasKey('class') : inflateObject(data) : data;
}

dynamic inflateObject<NestedTypes...>(Map map) {
  String className = map['class'];
  var properties = map.entries.where(x => x.key != 'class');
  var result = invoke<getType<NestedTypes...>(className)>(method: '(init)', target: null, args: []);
  for (var entry in jsonData.entries) {
    invoke(method: entry.key, target: result, args: [inflate(entry.value)], isSetter: true);
  }
}

This could be simplified for users if you allowed reflection into variadic generic parameters at compile time:

T myParse<T>(String json) => parse<getRecursiveTypes<T>>(json);

Summary

Features requested:

  • invoke function for dynamically calling methods on objects or classes
  • variadic generics so we can pass in multiple types to generics
  • getType function that transforms variadic generic arguments to one type
  • <optional> allow users to implement getType themselves
  • <optional> getRecursiveTypes function that expands one type to all of its nested types
@gaaclarke gaaclarke added the feature Proposed language feature that solves one or more problems label Feb 14, 2025
@gaaclarke gaaclarke changed the title Easy JSON parsing with invoke function and variadic generics Easy typesafe JSON parsing with invoke function and variadic generics Feb 14, 2025
@jakemac53
Copy link
Contributor

Have you seen Function.apply, which is partially what you are asking for here it seems? The argument names are symbols though instead of strings which is awkward to deal with, but it makes sense when you consider that the argument names can get mangled, I think the symbols can guarantee the same mangling (although don't quote me on that).

In general though I think we would prefer people do not use Function.apply and its performance is quite poor.

@gaaclarke
Copy link
Author

I talked a bit offline about this and a good point was made that this likely isn't possible because the tree-shaker can rip out methods that aren't used. invoke would require us to keep around methods that don't have call sites. I was thinking things were ripped out at the class level.

You could fix that by having a annotation to avoid a class and all of it's methods from being tree shaken. That's not ideal though.

We do have noSuchMethod so a lot of the information about methods still exists at runtime but it sounds like Dart has been trying to scale back some of its ability.

@gaaclarke

This comment has been minimized.

@gaaclarke
Copy link
Author

this likely isn't possible because the tree-shaker can rip out methods that aren't used

We could solve that with a magic type called DoNotTreeShake that blocks it from getting removed and using generic type constraints.

dynamic parse<NestedTypes... extends DoNotTreeShake>(String json) { ... }

@lrhn
Copy link
Member

lrhn commented Feb 15, 2025

Object had a method that allowed people to generate Invocation objects and send them to be dispatched one could write json deserializers that don't require code generation or runtime reflection.

It's not reflection reflection, but it's darn close. You are dynamically looking up a static function using a value for the name (not a name that occurred in the source). Then you invoke it with an argument list that didn't exist in the source either. If it's generic, I don't know how you would provide the type arguments.

The name resolution at runtime, using a value as name, is refection.
The invocation using dynamically structured arguments is very close (Function.apply is the most dynamic feature in the current Dart, outside of dart:mirrors, much more than normal dynamic invocations, because it covers Symbol name values to source names. It probably belongs in dart:mirrors.)

As mentioned, this has many of the same issues that makes AoT-compiling dart:mirrors prohibitively expensive. And that's because it really is a limited form of reflection.

@mraleph
Copy link
Member

mraleph commented Feb 19, 2025

I have been working on an alternative take on this problem (including a prototype). I have now ready to share: see #4271

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Proposed language feature that solves one or more problems
Projects
None yet
Development

No branches or pull requests

4 participants