Groups keyboard shortcuts have been updated
Dismiss
See shortcuts

Serialize JSObjects to JSON.

120 views
Skip to first unread message

Yordan Atanasov

unread,
Aug 8, 2024, 10:27:54 AM8/8/24
to TeaVM
Hello everyone,

I am currently building an application, which needs to send out RPC http calls to a remote server. My RPC body is expected to be of this format: 
{"id": number, "jsonrpc"string, "method": string, "params": Map<string,object>}

I created a class for the implementation, which I will attach as a screenshot. I need to serialize this class to the above mentioned json format. I was thinking of doing one of the following:

- Option 1 ( less preferred) Create a custom JSON serializer using reflection (if possible). I would need to have access to the field names of the class in order to do that. As far as I saw from the documentation it is possible to use reflection to do so https://teavm.org/docs/runtime/java-classes.html

- Option 2 Use TeaVMs JSON parser in order to stringify the created JSObject (RpcRequest). 

When I try option 2 I get some unexpected results (check screenshot of chrome inspector). I create an instance of RpcRequest, which implements JSObject and write it to object1 variable. As you can see I get extra data, the $id$ field and also every other fields gets a $ prefix. After using JSON.stringify on the object I get the value stored in jsontest2, which is quite different from what I envision from a serialized result of my object. 

Thanks in advance for any help I can get.
Jordan
Screenshot 2024-08-08 at 17.18.36.png
Screenshot 2024-08-08 at 16.54.27.png

Yordan Atanasov

unread,
Aug 12, 2024, 5:31:34 AM8/12/24
to TeaVM
With some help from the latest release notes I managed to fix the issue. I had to annotate the RpcRequest class with @JSClass(transparent = true) while implementing the JSObject interface.

Now I have a different issue. When trying to access class fields via TClass.getDeclaredField(String) I always get a null vlaue for the getter and setter. I've tried the following:
  • Annotating the class with JSClass
  • Implementing JSObject
  • Having native getters and setters annotated with @JSProperty
  • Each of these annotations by themselves.
Nothing seems to work. Then It all spirals down to TField 
public void checkSetAccess() throws TIllegalAccessException {
if (setter == null) {
throw new TIllegalAccessException();
    }
}

PS. I haven't done anything related to opening up reflection as noted here https://teavm.org/docs/runtime/java-classes.html. Would that be required?

Alexey Andreev

unread,
Aug 12, 2024, 5:59:46 AM8/12/24
to te...@googlegroups.com


- Option 1 ( less preferred) Create a custom JSON serializer using reflection (if possible). I would need to have access to the field names of the class in order to do that. As far as I saw from the documentation it is possible to use reflection to do so https://teavm.org/docs/runtime/java-classes.html.

With reflection it won't be possible, because currently annotations aren't supported on fields and methods.


- Option 2 Use TeaVMs JSON parser in order to stringify the created JSObject (RpcRequest). 

When I try option 2 I get some unexpected results (check screenshot of chrome inspector). I create an instance of RpcRequest, which implements JSObject and write it to object1 variable. As you can see I get extra data, the $id$ field and also every other fields gets a $ prefix. After using JSON.stringify on the object I get the value stored in jsontest2, which is quite different from what I envision from a serialized result of my object. 

Sure, because your code is completely broken. I don't even understand how do you suppose it to work, which means I don't follow mental model you built for JSO. It should be something like

public interface RpcRequest extends JSObject {
   @JSProperty("jsonrpc")
   void setJsonRpc(String value);

   // etc
}

var rpcRequest = JSObjects.<RpcRequest>create();
rpcRequest.setJsonRpc("system_name");

I also must mention option 3, which I consider the right approach: write a compile-time code generator for serialization, using, for example, annotation processors. There's also TeaVM metaprogramming API and Flavour project,  which I abandoned, and which included mapper that supports Jackson annotations.

Now I have a different issue. When trying to access class fields via TClass.getDeclaredField(String) I always get a null vlaue for the getter and setter. I've tried the following:
Please, don't use classlib classes directly. And I'm not sure reflection will work properly with JSO.

PS. I haven't done anything related to opening up reflection as noted here https://teavm.org/docs/runtime/java-classes.html. Would that be required?
Yes, that's mandatory. Without  explicit annotation reflection won't work at all.

PS: don't you EVER post code examples as screenshots.


Message has been deleted

Yordan Atanasov

unread,
Aug 12, 2024, 9:57:23 AM8/12/24
to TeaVM
     "Sure, because your code is completely broken. I don't even understand how do you suppose it to work, which means I don't follow mental model you built for JSO. It should be                    something like

    public interface RpcRequest extends JSObject {
       @JSProperty("jsonrpc")
       void setJsonRpc(String value);

       // etc
    }

    var rpcRequest = JSObjects.<RpcRequest>create();
    rpcRequest.setJsonRpc("system_name");"


I had gotten it to work in the way you mentioned by using JSObjects. However, I still can't grasp how to get constructor initialization to work as stated in the docs:

    Previously, to declare a constructor for a JS class, you had to define static factory method, annotated with @JSBody and some JS within. Now, it's possible to        declare a non-abstract overlay class (should be additionally annotated with @JSClass) with constructors. To instantiate such classes, you can use new syntax,      as you used for normal Java classes.


@JSClass(transparent = true)
public class RpcRequest implements JSObject {
private JSString jsonrpc; // JSON-RPC version
private JSString method; // The name of the method being called
private JSMap<JSString, JSObject> params; // Parameters to be passed to the method
private int id; // Unique identifier for the request

public RpcRequest() {
}

public RpcRequest(JSString jsonrpc, JSString method, JSMap<JSString, JSObject> params, int id) {
this.jsonrpc = jsonrpc;
this.method = method;
this.params = params;
this.id = id;
}

@JSProperty
public native JSString getJsonrpc();

@JSProperty
public native JSString getMethod();

@JSProperty
public native void setMethod(JSString method);

@JSProperty
public native JSMap<JSString, JSObject> getParams();

@JSProperty
public native void setParams(JSMap<JSString, JSObject> params);

@JSProperty
public native int getId();

@JSProperty
public native void setId(int id);
}

Trying to instantiate like so in a different java class ends up with a "RpcRequest is not defined". What am I missing here? I also tried annotating the constructors with the JSExport annotation as it was the only one applicable for constructors.

RpcRequest request = new RpcRequest(JSString.valueOf("2.0"),
JSString.valueOf("test"),
new JSMap<>(),
1);

    "Please, don't use classlib classes directly. And I'm not sure reflection will work properly with JSO."
I wasn't doing that. In my java code I try to access 
private static Field findField(Class<?> clazz, String fieldName) {
try {
return clazz.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
throw new IllegalStateException("Field " + fieldName + " does not exist in " + clazz.getName());
}
}
TeaVM call the classlib files by itself. Indeed it seems to work weirdly with JSO. JSClass jsClass = (JSClass) getPlatformClass().getMetadata(); always returns a JSWrapper. If I understand correctly in order to make use of reflection I need to implement what's mentioned in the Tuning java class library and use it only with non JSO objects.

Alexey Andreev

unread,
Aug 12, 2024, 10:09:43 AM8/12/24
to te...@googlegroups.com

    var rpcRequest = JSObjects.<RpcRequest>create();
    rpcRequest.setJsonRpc("system_name");"


I had gotten it to work in the way you mentioned by using JSObjects. However, I still can't grasp how to get constructor initialization to work as stated in the docs:

    Previously, to declare a constructor for a JS class, you had to define static factory method, annotated with @JSBody and some JS within. Now, it's possible to        declare a non-abstract overlay class (should be additionally annotated with @JSClass) with constructors. To instantiate such classes, you can use new syntax,      as you used for normal Java classes.

Sorry, read again. You just cited my code. Please, don't use classes, don't use @JSClass, nor any of approaches you suggested in this topic. Use following approach to create and fill empty JS objects:

    var rpcRequest = JSObjects.<RpcRequest>create();
    rpcRequest.setJsonRpc("system_name");"

Actually, you don't even need to declare RpcRequest. It's just for having strongly typed interop with JS, but you can still write as follows:

    var rpcRequest = JSObject.<JSMapLike<JSObject>>create();
    rpcRequest.set("jsonrpc", JSString.valueOf("system_name"));
Trying to instantiate like so in a different java class ends up with a "RpcRequest is not defined". What am I missing here?

What you written can be read as follows: "there's an RpcRequest class somewhere in JS runtime, here's it's description for Java". Since there's no RpcRequest class in the browser, you get runtime exception.

TeaVM call the classlib files by itself. Indeed it seems to work weirdly with JSO. JSClass jsClass = (JSClass) getPlatformClass().getMetadata(); always returns a JSWrapper. 

Sure. JS objects are not really Java objects. For example, if we have a JavaScript class like HTMLElement, there would no be any Java description for it anywhere, since browser itself does not know anything about Java. TeaVM does its best to somehow emulate this for you, but still the perfect emulation is either hard to implement or literally impossible.

If I understand correctly in order to make use of reflection I need to implement what's mentioned in the Tuning java class library and use it only with non JSO objects. --
Right, but it still won't work with JSO.

Yordan Atanasov

unread,
Aug 12, 2024, 10:47:44 AM8/12/24
to TeaVM
    "Sorry, read again. You just cited my code. Please, don't use classes, don't use @JSClass, nor any of approaches you suggested in this topic. Use following approach to create and fill        empty JS objects"

I see what you mean, but isn't the idea behind what I quoted from the docs to be able to substitute JSObjects.<RpcRequest>create(); + setters with the use of java constructors?
Now, it's possible to        declare a non-abstract overlay class (should be additionally annotated with @JSClass) with constructors
If I understand things correctly an "overlay" is a java representation of a JS class. We can declare it via an interface, memberless abstract class  or since 0.10.0 via a non-abstract class annotated with @JSClass, which has a constructor (not sure about member fields). Your JSObjects solution works perfectly, I just want to understand the new constructor part.

This should conform with the new changes in 0.10.0
@JSClass
public class RpcRequest implements JSObject {

public RpcRequest() {

}

@JSProperty
public native JSString getJsonrpc();

@JSProperty
public native void setJsonrpc(JSString jsonrpc);


@JSProperty
public native JSString getMethod();

@JSProperty
public native void setMethod(JSString method);

@JSProperty
public native JSMap<JSString, JSObject> getParams();

@JSProperty
public native void setParams(JSMap<JSString, JSObject> params);

@JSProperty
public native int getId();

@JSProperty
public native void setId(int id);
}

RpcRequest request = new RpcRequest();
request.setJsonrpc(JSString.valueOf("2.0"));
request.setParams(new JSMap<>());
request.setId(1);
request.setMethod(JSString.valueOf("system_chain"));

Maybe I understand this wrong and the ctors are only accessible if you import the class inside of a JS file and use the constructor from there.

Thank you for your replies by the way.

Alexey Andreev

unread,
Aug 12, 2024, 10:59:30 AM8/12/24
to te...@googlegroups.com


    "Sorry, read again. You just cited my code. Please, don't use classes, don't use @JSClass, nor any of approaches you suggested in this topic. Use following approach to create and fill        empty JS objects"

I see what you mean, but isn't the idea behind what I quoted from the docs to be able to substitute JSObjects.<RpcRequest>create(); + setters with the use of java constructors?

I just don't follow your mental model, sorry.


Now, it's possible to        declare a non-abstract overlay class (should be additionally annotated with @JSClass) with constructors
If I understand things correctly an "overlay" is a java representation of a JS class. We can declare it via an interface, memberless abstract class  or since 0.10.0 via a non-abstract class annotated with @JSClass, which has a constructor (not sure about member fields). Your JSObjects solution works perfectly, I just want to understand the new constructor part.

It's about JavaScript classes with constructors. For example, Promise has constructor, but prior 0.10 it was impossible to declare it as a constructor, a static method annotated with @JSBody should be declared instead



This should conform with the new changes in 0.10.0
@JSClass
public class RpcRequest implements JSObject {

public RpcRequest() {
}

@JSProperty
public native JSString getJsonrpc();

@JSProperty
public native void setJsonrpc(JSString jsonrpc);

@JSProperty
public native JSString getMethod();

@JSProperty
public native void setMethod(JSString method);

@JSProperty
public native JSMap<JSString, JSObject> getParams();

@JSProperty
public native void setParams(JSMap<JSString, JSObject> params);

@JSProperty
public native int getId();

@JSProperty
public native void setId(int id);
}

If you have your own hand-written JavaScript code that you load prior to TeaVM-generated JS and which contains something like

class RpcRequest {
   jsonrpc
   method
   params
   id

   constructor() {
   }
}

Then yes, this should work.

RpcRequest request = new RpcRequest();
request.setJsonrpc(JSString.valueOf("2.0"));
request.setParams(new JSMap<>());
request.setId(1);
request.setMethod(JSString.valueOf("system_chain"));

Maybe I understand this wrong and the ctors are only accessible if you import the class inside of a JS file and use the constructor from there.

This depends on what you was going to do, but since I don't follow you mental model, I can't give a hint.

Ihromant

unread,
Aug 14, 2024, 10:01:17 AM8/14/24
to TeaVM
As an alternative I can suggest to use my library: https://github.com/ihromant/teavm-io
Using it you will be able to have just class
public class RPCRequest implements Message {
private String jsonRPC;
private int type; // or some enum
private String method;
private Map<String, String> params;
// getters/setters
}
When you send type, you will be able to decode what was sent on server and deserialize it there using Jackson or other JS parser. On client it will be handled as Java object as well and serialized/deserialized using Converters.
This library is simple convention-over-configuration library which corresponds to next Jackson configuration:
private final ObjectMapper map = JsonMapper.builder()
.serializationInclusion(JsonInclude.Include.NON_NULL)
.enable(SerializationFeature.WRITE_ENUMS_USING_INDEX)
.configure(MapperFeature.PROPAGATE_TRANSIENT_MARKER, true)
.build();
Limitations:
1. All classes you send should implement Message.
2. Just combined types from String, int, double, boolean, List, Map, arrays are supported.

Using it I avoid DTO layer and have client-to-server and server-to-client transfer of ordinary Java objects.
понеділок, 12 серпня 2024 р. о 16:57:23 UTC+3 yordan....@limechain.tech пише:
Reply all
Reply to author
Forward
0 new messages