manifold-systems / manifold

Manifold is a Java compiler plugin, its features include Metaprogramming, Properties, Extension Methods, Operator Overloading, Templates, a Preprocessor, and more.
http://manifold.systems/
Apache License 2.0
2.42k stars 125 forks source link

fasterxml.jackson can't create an instance of a manifold JSON object #567

Closed dakotahNorth closed 7 months ago

dakotahNorth commented 7 months ago

Trying to create an object to use with fasterxml/jackson

This code worked when ExampleEvent was a concrete Java class that is simply a Java object for a JSON class.

private Consumer<JsonNode> createHandler(Method method, Class<?> eventClass, Object bean) {
    return jsonNode -> {
        try {
            Object event = objectMapper.treeToValue(jsonNode, eventClass);
            method.setAccessible(true);
            method.invoke(bean, event);
        } catch (Exception e) {
            throw new RuntimeException("Error invoking handler method: " + method.getName(), e);
        }
    };
  }

And the concrecte Java class

/**
 * 
 * This class should align with the JSON message structure being handled.
 *
 * Key Points:
 * - Default Constructor: Necessary for JSON deserialization. Frameworks like Jackson
 *   require a no-argument constructor to instantiate the object before populating its fields.
 * - Field with Getter and Setter: The `data` field represents the content of the event.
 *   Getters and setters are essential for accessing the field's value and for Jackson to
 *   deserialize JSON into this object.
 * - toString Method: Provides a string representation of the object, useful for logging
 *   and debugging purposes.
 */
@JsonIgnoreProperties(ignoreUnknown = true)
public static class ExampleEvent {

    private String data; // The data field of the event

    /**
     * Default constructor is required for frameworks like Jackson to deserialize
     * JSON content into a MyEvent object.
     */
    public ExampleEvent() {
    }

    /**
     * Constructs a MyEvent with the specified data.
     * @param data The data content of the event.
     */
    public ExampleEvent(int data) {

    }

    /**
     * Gets the data content of the event.
     * @return The data content of the event.
     */
    public String getData() {
        return data;
    }

    /**
     * Sets the data content of the event.
     * @param data The data content to set.
     */
    public void setData(String data) {
        this.data = data;
    }

    /**
     * Returns a string representation of the MyEvent object, which includes its data content.
     * Useful for logging and debugging.
     * @return A string representation of the MyEvent object.
     */
    @Override
    public String toString() {
        return "MyEvent{" +
            "data='" + data + '\'' +
            '}';
    }
}

But, this seemed like a perfect example to use manifold JSON schema to define the class.

  {
    "$schema": "http://json-schema.org/draft-07/schema#",
    "$id": "http://com.example.solace.springboot.starter.messaging.ExampleEvent.json",
    "title": "ExampleEvent",
    "type": "object",
    "properties": {
      "messageType": { "type": "string" },
      "data": { "type": "string" }
    },
    "required": ["messageType", "data"]
  }

But when using the Manifold class, the following error occrus.

   Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of 
   `com.example.solace.springboot.starter.messaging.ExampleEvent`    
   (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, 
   have custom deserializer, or contain additional type information

Should JSON created object have a default constructor?

How to handle the ability for @JsonIgnoreProperties(ignoreUnknown = true)?

rsmckinney commented 7 months ago

Have not used jackson with manifold-json, but the jackson documentation claims if you include the javac -parameters argument, all the wiring annotations are not required. Have you tried that?

dakotahNorth commented 7 months ago

Just. tried that. Changed the build to use -parameters ... same issue.

The issue arises from

Object event = objectMapper.treeToValue(jsonNode, eventClass);

Where ObjectMapper.treeToValue expects a class with a default constructor and JavaBean methods. The runtime exception above is when treeToValue tries to instantiate the eventClass with default constructor.

Jackson also supports using a builder ... but then I would need to annotate the JSON object. Similar to below ...

@JsonDeserialize(builder = ExampleEvent.ExampleEventBuilder.class)
public class ExampleEvent {
    private final String data;

    private ExampleEvent(String data) {
        this.data = data;
    }

    // Getter
    public String getData() {
        return data;
    }

    // Builder
    public static class ExampleEventBuilder {
        private String data;

        public ExampleEventBuilder withData(String data) {
            this.data = data;
            return this;
        }

        public ExampleEvent build() {
            return new ExampleEvent(data);
        }
    }
}

Researching Jackson to see if builder pattern can be configured as the default so the annotation isn't needed.

dakotahNorth commented 7 months ago

There was no easy way for Jackson to switch over without creating custom bindings.

Instead ported code to FastJSON, which worked really nicely with Manifold.

rsmckinney commented 7 months ago

Good to know re FastJSON. Curious to know why you aren't just using manifold-json, why FastJson at all?

dakotahNorth commented 7 months ago

Oh ... right.

I have a general message handler that parses all JSON into a JSONObject and then passes that to the message handler.

void onMessageReceived(String messagePayload) {
    try {
        JSONObject rootNode = JSON.parseObject(messagePayload);
        String messageType = rootNode.getString("messageType");
        List<Consumer<JSONObject>> handlers = messageTypeHandlers.get(messageType);
        if (handlers != null && !handlers.isEmpty()) {
            handlers.forEach(handler -> handler.accept(rootNode));
        } else {
            System.out.println("Unhandled messageType: " + messageType);
        }
    } catch (RuntimeException e) {
        e.printStackTrace();
    }
}

And shows up as an ExampleEvent (from JSON) in MessageHandler:

@MessageHandler
public final void handleExampleEvent(ExampleEvent event) { receivedEvents.add(event); }

Could I have done that with manifold?

rsmckinney commented 7 months ago

Could I have done that with manifold?

From what I can gather, yes. You can use manifold.json.rt.Json.fromJson(String) in place of JSON.parseObject(String).

  void onMessageReceived(String messagePayload) {
    try {
      Bindings rootNode = (Bindings) Json.fromJson(messagePayload);
      String messageType = (String) rootNode.get("messageType");
      List<Consumer<Bindings>> handlers = messageTypeHandlers.get(messageType);
      if (handlers != null && !handlers.isEmpty()) {
        handlers.forEach(handler -> handler.accept(rootNode));
      } else {
        System.out.println("Unhandled messageType: " + messageType);
      }
    } catch (RuntimeException e) {
      e.printStackTrace();
    }
  }

Taking it to another level, the message payload could be made type-safe instead of using untyped JsonObject/Bindings. Given messageType can map to a Java type corresponding with manifold-json, consumers of events could be hardcore type-safe. Think Consumer<T> as opposed to Consumer<JsonNode>. But that may not make sense depending on your problem space.

This could be achieved using the Xxx.load().fromJson(jsonString) where Xxx is a manifold-json derived type. Since you are loading these dynamically, you would have to use this API reflectively.

    String jsonJavaType = (String) rootNode.get("messageType");
    Loader<?> loader = (Loader<?>) ReflectUtil.method(jsonJavaType, "load").invokeStatic();
    Object typesafeObj = loader.fromJson(rootNode.get("message"));
    ...
    List<Consumer> handlers = messageTypeHandlers.get(messageType);
    handlers.forEach(handler -> handler.accept(typesafeObj)); // type-safe at method receiver

Where handler could be something like:

public class MyMessageConsumer implements Consumer<MyMessageType> {
  public void accept(MyMessageType t) {
    . . .
  }
}

Anyhow, food for thought.

dakotahNorth commented 7 months ago

That is super helpful ... thank you!