1

I have a class representing the root node and want to deserialize data to different subclasses based on the value of action. The fields must be final.

@EqualsAndHashCode
@ToString
public final class SeMessage {

  @Getter
  private final String action;

  @Getter
  private final SeMessageData data;

  @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
  public SeMessage(
      @JsonProperty("action") final String action,
      @JsonProperty("data")
      @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "action", include = JsonTypeInfo.As.EXTERNAL_PROPERTY)
      @JsonSubTypes({
          @JsonSubTypes.Type(value = Se155QuestionsActiveMessageData.class, name = "155-questions-active")
      }) final SeMessageData data
  ) {
    super();

    this.action = action;
    this.data = data;
  }

}

Here are SeMessageData and Se155QuestionsActiveMessageData:

public abstract class SeMessageData {

  SeMessageData() {
    super();
  }

}
@EqualsAndHashCode
@ToString
public final class Se155QuestionsActiveMessageData extends SeMessageData {

  @Getter
  private final String siteBaseHostAddress;

  @Getter
  private final Long id;

  @Getter
  private final String titleEncodedFancy;

  @Getter
  private final String bodySummary;

  @Getter
  private final List<String> tags;

  @Getter
  private final Long lastActivityDate;

  @Getter
  private final String url;

  @Getter
  private final String ownerUrl;

  @Getter
  private final String ownerDisplayName;

  @Getter
  private final String apiSiteParameter;

  @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
  public Se155QuestionsActiveMessageData(
      @JsonProperty("siteBaseHostAddress") final String siteBaseHostAddress,
      @JsonProperty("id") final Long id,
      @JsonProperty("titleEncodedFancy") final String titleEncodedFancy,
      @JsonProperty("bodySummary") final String bodySummary,
      @JsonProperty("tags") final List<String> tags,
      @JsonProperty("lastActivityDate") final Long lastActivityDate,
      @JsonProperty("url") final String url,
      @JsonProperty("ownerUrl") final String ownerUrl,
      @JsonProperty("ownerDisplayName") final String ownerDisplayName,
      @JsonProperty("apiSiteParameter") final String apiSiteParameter
  ) {
    super();

    this.siteBaseHostAddress = siteBaseHostAddress;
    this.id = id;
    this.titleEncodedFancy = titleEncodedFancy;
    this.bodySummary = bodySummary;
    this.tags = tags;
    this.lastActivityDate = lastActivityDate;
    this.url = url;
    this.ownerUrl = ownerUrl;
    this.ownerDisplayName = ownerDisplayName;
    this.apiSiteParameter = apiSiteParameter;
  }

}

Topic #1:

Doing so causes an exception to be thrown:

com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `com.oliveryasuna.stackexchange.websocket.message.data.Se155QuestionsActiveMessageData` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('{"siteBaseHostAddress":"stackoverflow.com","id":70098765,"titleEncodedFancy":"How to avoid the command execution when appending lines to a file","bodySummary":"I'm trying to save the content of script into a file using command line, but I noticed that when the tee command detects linux commands such as $(/usr/bin/id -u), it execute the commands rather than ...","tags":["linux","append","tee"],"lastActivityDate":1637767762,"url":"https://stackoverflow.com/questions/70098765/how-to-avoid-the-command-execution-when-appending-lines-to-a-file","ownerUrl":"https://stackoverflow.com/users/17499564/alex","ownerDisplayName":"Alex","apiSiteParameter":"stackoverflow"}')
 at [Source: (String)"{"action":"155-questions-active","data":"{\"siteBaseHostAddress\":\"stackoverflow.com\",\"id\":70098765,\"titleEncodedFancy\":\"How to avoid the command execution when appending lines to a file\",\"bodySummary\":\"I'm trying to save the content of script into a file using command line, but I noticed that when the tee command detects linux commands such as $(/usr/bin/id -u), it execute the commands rather than ...\",\"tags\":[\"linux\",\"append\",\"tee\"],\"lastActivityDate\":1637767762,\"url\":\"[truncated 248 chars]; line: 1, column: 748]
    at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:63)
    at com.fasterxml.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1588)
    at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1213)
    at com.fasterxml.jackson.databind.deser.std.StdDeserializer._deserializeFromString(StdDeserializer.java:311)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromString(BeanDeserializerBase.java:1495)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeOther(BeanDeserializer.java:207)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:197)
    at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer._deserialize(AsArrayTypeDeserializer.java:120)
    at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer.deserializeTypedFromObject(AsArrayTypeDeserializer.java:61)
    at com.fasterxml.jackson.databind.deser.AbstractDeserializer.deserializeWithType(AbstractDeserializer.java:263)
    at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:539)
    at com.fasterxml.jackson.databind.deser.impl.ExternalTypeHandler._deserialize(ExternalTypeHandler.java:359)
    at com.fasterxml.jackson.databind.deser.impl.ExternalTypeHandler.complete(ExternalTypeHandler.java:302)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeUsingPropertyBasedWithExternalTypeId(BeanDeserializer.java:1090)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeWithExternalTypeId(BeanDeserializer.java:931)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:360)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:195)
    at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:322)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4593)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3548)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3516)
    at com.oliveryasuna.stackexchange.websocket.SeWebSocketHandler.handleTextMessage(SeWebSocketHandler.java:56)
    at org.springframework.web.socket.handler.AbstractWebSocketHandler.handleMessage(AbstractWebSocketHandler.java:43)
    at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter.handleTextMessage(StandardWebSocketHandlerAdapter.java:114)
    at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter.access$000(StandardWebSocketHandlerAdapter.java:43)
    at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter$3.onMessage(StandardWebSocketHandlerAdapter.java:85)
    at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter$3.onMessage(StandardWebSocketHandlerAdapter.java:82)
    at org.apache.tomcat.websocket.WsFrameBase.sendMessageText(WsFrameBase.java:415)
    at org.apache.tomcat.websocket.WsFrameBase.processDataText(WsFrameBase.java:515)
    at org.apache.tomcat.websocket.WsFrameBase.processData(WsFrameBase.java:301)
    at org.apache.tomcat.websocket.WsFrameBase.processInputBuffer(WsFrameBase.java:133)
    at org.apache.tomcat.websocket.WsFrameClient.processSocketRead(WsFrameClient.java:95)
    at org.apache.tomcat.websocket.WsFrameClient.resumeProcessing(WsFrameClient.java:212)
    at org.apache.tomcat.websocket.WsFrameClient.access$500(WsFrameClient.java:31)
    at org.apache.tomcat.websocket.WsFrameClient$WsFrameClientCompletionHandler.doResumeProcessing(WsFrameClient.java:189)
    at org.apache.tomcat.websocket.WsFrameClient$WsFrameClientCompletionHandler.completed(WsFrameClient.java:163)
    at org.apache.tomcat.websocket.WsFrameClient$WsFrameClientCompletionHandler.completed(WsFrameClient.java:148)
    at org.apache.tomcat.websocket.AsyncChannelWrapperSecure$WrapperFuture.complete(AsyncChannelWrapperSecure.java:471)
    at org.apache.tomcat.websocket.AsyncChannelWrapperSecure$ReadTask.run(AsyncChannelWrapperSecure.java:338)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
    at java.base/java.lang.Thread.run(Thread.java:829)

I tried using a custom deserializer, but the same exception is thrown. You'll notice that this uses regex, which is my ultimate goal (see Topic #2).

final class Deserializer extends StdDeserializer<SeMessage> {

  private static final Map<String, Class<? extends SeMessageData>> ACTION_TYPE_MAP = Map.ofEntries(
      Map.entry("^155-questions-active$", Se155QuestionsActiveMessageData.class),
      Map.entry("^(?!155)\\d+-questions-(?:active|newest(?:-tag-[a-z0-9]+)?)$", SeQuestionsMessageData.class)
  );

  private Deserializer() {
    super(SeMessage.class);
  }

  @Override
  public final SeMessage deserialize(final JsonParser parser, final DeserializationContext context) throws IOException, JsonProcessingException {
    final JsonNode json = parser.getCodec().readTree(parser);

    final String action = json.get("action").textValue();

    for(final Map.Entry<String, Class<? extends SeMessageData>> actionTypeEntry : ACTION_TYPE_MAP.entrySet()) {
      final String actionRegex = actionTypeEntry.getKey();

      if(action.matches(actionRegex)) {
        final Class<? extends SeMessageData> type = actionTypeEntry.getValue();

        final JsonNode dataJson = json.get("data");

        final SeMessageData data = parser.getCodec().treeToValue(dataJson, type);

        return new SeMessage(action, data);
      }
    }

    throw new IOException("Unsupported action: " + action + ".");
  }

}

Odd enough, if I deserialize data with the followingly (instead of treeToValue), no exception is thrown. The OBJECT_MAPPER is just a plain instance of ObjectMapper without any added modules or configuration.

final String dataJson = json.get("data").textValue();

final SeMessageData data = JacksonUtil.OBJECT_MAPPER.readValue(dataJson, type);

return new SeMessage(action, data);

Topic #2:

Once Topic #1 is resolved, I will still want to avoid using a custom deserializer. You'll notice in the custom deserialize class above that I match the value of action with regex. Is it possible to override the functionality of @JsonTypeInfo and @JsonSubTypes, such that the name argument is regex and the value of action is matched that way?

Oliver
  • 1,465
  • 4
  • 17

0 Answers0