cloudevents / sdk-java

Java SDK for CloudEvents
https://cloudevents.github.io/sdk-java/
Apache License 2.0
398 stars 160 forks source link

Unable to serialize complex (custom) extension in the expected json format #667

Open abhupadh opened 1 month ago

abhupadh commented 1 month ago

creating a custom extension object implementing CloudEventExtension, when using the extension (customExt) as below, and then serializing the CloudEvent, the serialization for the CloudEvent is failing with JsonMappingException.

CloudEventBuilder.v1()
        .withSource(URI.create(source))
        .withType(event_type)
        .withTime(event_time)
        .withId(event.getEventId())
        .withDataContentType(contentType)
        .withData(eventPayaload)
        .withDataSchema(URI.create(XDM_SCHEMA_URI))
        .withExtension(DATA_SCHEMA_VERSION_KEY_EXTENSION_NAME, schemaVerExt)
        .withExtension(X_ACTION_ID_KEY_EXTENSION_NAME, requestIdExt)
        .withExtension(customExt);

here is how the extension looks like. The customExt is the Recipient object Screenshot 2024-09-06 at 4 22 54 PM

Also, if using the other supported methods withExtension (with key, value) as below, I had to send the string version of the customExt, and then if serialized the output is not in the expected format (see below). It can break our customers event integrations. Kindly suggest.

CloudEventExtension_Methods

expected serialized CloudEvent custom extension

"recipient":{"userid":"9CE048DB5E8F485XXXXXXXXX@AdobeOrg"}

actual serialized CloudEvent custom extension

"recipient":"{\"userid\":\"9CE048DB5E8F485XXXXXXXXX@AdobeOrg\"}"

The serialization is failing while reading extensions here, as I believe the Object type value is not supported.

abhupadh commented 1 month ago

This expected json for extension was working fine with CloudEvent 1.0 , and ExtensionFormat / InMemoryFormat marshaling / unmarshaling feature.

shikhartanwar commented 1 month ago

+1

nicdard commented 1 month ago

+1

pierDipi commented 1 month ago

In the CloudEvents spec, the type system for metadata https://github.com/cloudevents/spec/blob/main/cloudevents/spec.md#type-system describes the type "String" but what you expect is a JSON value ?

"recipient":{"userid":"9CE048DB5E8F485XXXXXXXXX@AdobeOrg"}

Also please, add the full steps to reproduce the issue that I can and paste

abhupadh commented 1 month ago

Thanks @pierDipi for responding. Yes, that is the correct expectation (receiving a json value). Below is the code and steps to reproduce the issues while trying both the ways of setting an extension to a CloudEvent

  1. JsonMappingException if using a custom extension for a object
  2. Unexpected string instead of Json for a complex extension

------- Issue 1 --------- Recipient object

@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
public class Recipient {
  @JsonProperty("userid")
  private final String userid;

  @JsonProperty("clientid")
  private final String clientid;

  public Recipient(String userid) {
    this.userid = userid;
    this.clientid = null;
  }

  @JsonCreator
  public Recipient(
      @JsonProperty("userid") String userid, @JsonProperty("clientid") String clientid) {
    this.userid = userid;
    this.clientid = clientid;
  }

  // ------------------------------------------------------------------------------------------------
  // Accessors
  // ------------------------------------------------------------------------------------------------

  public String getUserid() {
    return userid;
  }

  public String getClientid() {
    return clientid;
  }

  // ------------------------------------------------------------------------------------------------

  @Override
  public String toString() {
    return "Recipient{" + "userid='" + userid + '\'' + ", clientid='" + clientid + '\'' + '}';
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Recipient recipient = (Recipient) o;
    return Objects.equals(userid, recipient.userid) && Objects.equals(clientid, recipient.clientid);
  }

  @Override
  public int hashCode() {
    return Objects.hash(userid, clientid);
  }
}

Creating a custom extension RecipientExtension using the Recipient object

public class RecipientExtension implements CloudEventExtension {

  public static final String RECIPIENT_KEY_EXTENSION_NAME = "recipient";
  private static final Set<String> KEY_SET = Set.of(RECIPIENT_KEY_EXTENSION_NAME);

  private Recipient recipient;

  public RecipientExtension(String userId, String clientId) throws IOException {
    this.recipient = new Recipient(userId, clientId);
  }

  public Recipient getRecipient() {
    return recipient;
  }

  @Override
  public void readFrom(CloudEventExtensions cloudEventExtensions) {
    Object tp = cloudEventExtensions.getExtension(RECIPIENT_KEY_EXTENSION_NAME);
    if (tp != null) {
      this.recipient = tp instanceof Recipient ? (Recipient) tp : null;
    }
  }

  @Override
  public Object getValue(String key) throws IllegalArgumentException {
    switch (key) {
      case RECIPIENT_KEY_EXTENSION_NAME:
        return this.recipient;
      default:
        throw ExtensionUtils.generateInvalidKeyException(this.getClass(), key);
    }
  }

  @Override
  public Set<String> getKeys() {
    return KEY_SET;
  }

  @Override
  public String toString() {
    return "RecipientExtension{" + "recipient=" + recipient + '}';
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    RecipientExtension that = (RecipientExtension) o;
    return Objects.equals(recipient, that.recipient);
  }

  @Override
  public int hashCode() {
    return Objects.hash(recipient);
  }
}

Create the CloudEvent object using CloudEventBuilder.v1()

private static CloudEvent getCloudEvent()
      throws JsonProcessingException {
    return CloudEventBuilder.v1()
        .withSource(URI.create("some_source"))
        .withType("some_event_type")
        .withId("some_id")
        .withDataContentType("application/json")
        .withData(JsonCloudEventData.wrap(convertToJsonNode("some_data")))
        .withDataSchema(URI.create("some_schema"))
        .withExtension("requestidext", "some_request_id")
        .withExtension(new RecipientExtension(userId, null))
        .build();
  }

Now serialize the CloudEventV1 object

public static final SimpleModule simpleModule =
      getCloudEventJacksonModule(JsonFormatOptions.builder().build());

  private static final ObjectMapper OBJECT_MAPPER =
      JsonMapper.builder()
          .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
          .serializationInclusion(Include.NON_NULL)
          .addModule(
              simpleModule)
          .build();

public static String serialize(final CloudEvent object) throws JsonProcessingException {
    Preconditions.checkNotNull(object, THE_OBJECT_INSTANCE_CAN_NOT_BE_NULL);
    return OBJECT_MAPPER.writeValueAsString(object);
 }

the above serialization will fail with JsonMappingException with the underlying stacktrace showing failing at the readContext of CloudEventSerializer Screenshot 2024-10-01 at 11 32 36 AM Screenshot 2024-10-01 at 11 33 23 AM

-------- Issue 2 -------- Instead of using the custom extension object, if we use the serialized string of it using withExtension(String key, String value) we are not getting the json representation of it

var recipientExt = new RecipientExtension(userId, clientId);

get the Recipient object using the recipientExt.getRecipient() and set as extension while constructing CloudEvent with its serialized version

.withExtension("recipient", serialize(recipientExt.getRecipient()));
abhupadh commented 1 week ago

Hi @pierDipi, is there any update on this? We are blocked with our java17 upgrade due to this.