elnabo / json2object

Type safe Haxe/JSON (de)serializer
MIT License
66 stars 17 forks source link

Support Type Parameters #70

Closed olichose123 closed 3 years ago

olichose123 commented 4 years ago

A macro fatalError indicates that type parameters are not supported, so it is difficult to build some kind of generic serializer object, but I was able to make it work with anonymous structures, suggesting it is after all possible.

Considering the following serializer class. Note that while it is impossible to declare json2object.JsonParser<Serializable>, it is possible to declare an anonymous typedef.

class JsonSerializer<Serializable> extends Serializer<Serializable, String>
{
    public var parser:{function fromJson(jsonString:String, ?filename:String):Serializable;};
    public var writer:{function write(o:Serializable, ?space:String):String;};

    public function new(parser, writer):Void
    {
        super();
        this.parser = parser;
        this.writer = writer;
    }

    override public function serialize(obj:Serializable):String
    {
        return writer.write(obj);
    }

    override public function unserialize(data:String):Serializable
    {
        return parser.fromJson(data);
    }
}

To my surprise, the following code works correctly and the data is serialized then unserialized. This suggests that using type parameters would be possible. For now, I have to manually create the writer and parser, then pass them while hiding what they actually are. With type parameters, I could instantiate them. It seems to be a matter of using some kind of generic class, which I suppose would be the only way for type parameters to be supported as the majority of the logic is built with a build macro.

class MySerializable
{
    public var x:Int;
    public var y:Int;

    public function new():Void {}
}
var s:MySerializable = new MySerializable();
s.x = 100;
s.y = 121;
var serializer:JsonSerializer<MySerializable>;
serializer = new JsonSerializer(new json2object.JsonParser<MySerializable>(), new json2object.JsonWriter<MySerializable>());
var data = serializer.serialize(s);
var s2:MySerializable = serializer.unserialize(data);
trace(s2.x, s2.y);

So, is it possible to support type parameters but difficult, is my solution the only one for the moment, or is there an easier approach my dumb ass completely missed?

ibilon commented 4 years ago

You can't write json2object.JsonParser<Serializable> inside the JsonSerializer because the compiler doesn't the type at this point, and json2object needs to know the type so it can build a type safe (un)serializer.

Can you try to add the meta @:generic to your JsonSerializer class? It'll generate a version of it for each of the Serializable type parameter you pass, so json2object should have access to the concrete type.

I'm not 100% sure it'll work, it'll depends on which is run first, the @:generic or the json2object macro.

olichose123 commented 3 years ago

Totally forgot to answer this, but unfortunately it still doesn't work with generic. My approach right now is to use an autoBuild macro to add static parsers to specific objects.

With this macro every object with @:build(DefinitionMacro.build()) or extending an object with autoBuild will have a getParser and getListParser, which create a parser of T and a parser of Array respectively, then unserialize and unserializeList which call the get*Parser and return an object. Hacky, but good enough.

An interesting approach could be to have an autobuild macro on an interface JsonParsable and JsonWritable that, when added with implement JsonParsable, add an unserialize(json:String) static method, in a similar way to hxbit.

package macros;

import haxe.macro.Expr.Field;
import haxe.macro.Context;

class DefinitionMacro
{
    macro public static function build():Array<Field>
    {
        var fields = Context.getBuildFields();
        var type = Context.toComplexType(Context.getLocalType());

        var all:Field = {
            name: 'all',
            access: [APublic, AStatic],
            pos: Context.currentPos(),
            kind: FProp('default', 'null', macro:haxe.ds.StringMap<$type>, macro new haxe.ds.StringMap<$type>()),
        };

        var getParser:Field = {
            name: 'getParser',
            access: [APublic, AStatic],
            pos: Context.currentPos(),
            kind: FFun({
                args: [],
                ret: macro:json2object.JsonParser<$type>,
                expr: macro
                {return new json2object.JsonParser<$type>();}
            })
        }

        var getListParser:Field = {
            name: 'getListParser',
            access: [APublic, AStatic],
            pos: Context.currentPos(),
            kind: FFun({
                args: [],
                ret: macro:json2object.JsonParser<Array<$type>>,
                expr: macro
                {return new json2object.JsonParser<Array<$type>>();}
            })
        }

        var unserialize:Field = {
            name: 'unserialize',
            access: [APublic, AStatic],
            pos: Context.currentPos(),
            kind: FFun({
                args: [
                    {name: 'json', type: macro:String},
                    {name: 'context', type: macro:String, opt: true},
                ],
                ret: macro:$type,
                expr: macro
                {
                    var parser = getParser();
                    var definition:$type = parser.fromJson(json, context);
                    for (error in parser.errors)
                        holi.Log.trace(Std.string(error), context, Error);
                    all.set(definition.name, definition);
                    return definition;
                }
            })
        }

        var unserializeList:Field = {
            name: 'unserializeList',
            access: [APublic, AStatic],
            pos: Context.currentPos(),
            kind: FFun({
                args: [
                    {name: 'json', type: macro:String},
                    {name: 'context', type: macro:String, opt: true},
                ],
                ret: macro:Array<$type>,
                expr: macro
                {
                    var parser = getListParser();
                    var definitions:Array<$type> = parser.fromJson(json, context);
                    for (error in parser.errors)
                        holi.Log.trace(Std.string(error), context, Error);
                    for (definition in definitions)
                    {
                        all.set(definition.name, definition);
                    }
                    return definitions;
                }
            })
        }

        fields.push(all);
        fields.push(getParser);
        fields.push(getListParser);
        fields.push(unserialize);
        fields.push(unserializeList);

        return fields;
    }
}
olichose123 commented 3 years ago

Closing this for now. Totally forgot about it, sorry.

NotRoland commented 3 years ago

@olichose123 Hey, I'm also looking to make a generic deserializer / serializer, since it would really cut down on the amount of code I have to write for my project. Have you found a solution?

olichose123 commented 3 years ago

Yes and no. Though I never succeeded making a generic one working, here's a macro that works easily. It creates a public serialize() field on objects, and a static unserialize(json, context) where context would be a file name for better error tracing.

package my.macros;

import haxe.macro.Expr.Field;
import haxe.macro.Context;

class SerialMacros
{
    macro public static function build():Array<Field>
    {
        var fields = Context.getBuildFields();
        var type = Context.toComplexType(Context.getLocalType());

        var serialize:Field = {
            name: 'serialize',
            access: [APublic],
            pos: Context.currentPos(),
            kind: FFun({
                args: [],
                ret: macro:String,
                expr: macro
                {
                    var writer = new json2object.JsonWriter<$type>();
                    var json = writer.write(this, "    ");
                    return json;
                }
            })
        }

        var unserialize:Field = {
            name: 'unserialize',
            access: [APublic, AStatic],
            pos: Context.currentPos(),
            kind: FFun({
                args: [
                    {name: 'json', type: macro:String},
                    {name: 'context', type: macro:String, opt: true},
                ],
                ret: macro:$type,
                expr: macro
                {
                    var parser = new json2object.JsonParser<$type>();
                    var definition:$type = parser.fromJson(json, context);
                    for (error in parser.errors)
                        trace(Std.string(error));
                    return definition;
                }
            })
        }

        fields.push(serialize);
        fields.push(unserialize);

        return fields;
    }
}
@:build(my.macros.SerialMacros.build())
class MySerializableObject
{
    public var x:Int;
    public var y:String;

    public function new(x:Int, y:String):Void
    {
        this.x = x;
        this.y = y;
    }
}
public static function main():Void
{
        var obj = new MySerializableObject(20, "hello");
        trace(obj);
        var serialized = obj.serialize();
        var obj2 = MySerializableObject.unserialize(serialized);
        trace(serialized);
        trace(obj2);
}

Technically, but I haven't tested it, something like this using autoBuild would create a serializable base class without having to add macros to everything

@:autoBuild(my.macros.SerialMacros.build())
class Serializable {}
NotRoland commented 3 years ago

It does look like a build macro would work for my situation, so I'll try that. Many thanks for the quick and complete response!