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?