54

I am using Jackson for JSON (de)serialization in conjunction with Spring. However I am having an issue with a field being twice in some cases.

I have an abstract class:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "mimeType")
@JsonSubTypes({
    @JsonSubTypes.Type(value = ImageBookmarkJsonModel.class, name = "image/jpeg"),
    @JsonSubTypes.Type(value = EpubBookmarkJsonModel.class, name = "application/epub+zip")
})
public abstract class AbstractBookmarkJsonModel extends AbstractJsonModel {
    protected String mimeType;
    // Removed other fields for brevity

    public String getMimeType() {
        return mimeType;
    }

    public void setMimeType(String mimeType) {
        this.mimeType = mimeType;
    }

    @Override
    public String toString() {
        ObjectMapper mapper = new ObjectMapper();

        try {
            return mapper.writeValueAsString(this);
        } catch (IOException e) {
            throw new IllegalStateException("Cannot convert object of type " + this.getClass().toString() + " to JSON", e);
        }
    }
}

And a concrete class extend the abstract:

public class EpubBookmarkJsonModel extends AbstractBookmarkJsonModel {
    private static final long serialVersionUID = 1L;
    // Removed other fields for brevity

    public EpubBookmarkJsonModel() {
        this.mimeType = "application/epub+zip";
    }
}

The problem is that when I serialize this JSON, I get a duplicate mimeType field:

{
  "mimeType": "application/epub+zip",
  "mimeType": "application/epub+zip",
  "userId": 24,
  "acid": "ACID-000000000029087",
  "added": "2013-08-14T12:02:17Z",
  "epubBookmarkId": 34,
  "cfi": "epubcfi(/6/4!/2/68)",
  "context": "CONTEXT"
}

I have tried using the recommendation of previous answers to use the @JsonAutoDetect annotation to specify that only fields on a class should be used as well as setting the same on the ObjectMapper, however this does not fix the problem.

Annotation:

@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
        setterVisibility = JsonAutoDetect.Visibility.NONE, creatorVisibility = JsonAutoDetect.Visibility.NONE,
        isGetterVisibility = JsonAutoDetect.Visibility.NONE)

ObjectMapper:

    ObjectMapper mapper = new ObjectMapper();
    mapper.getSerializationConfig().getDefaultVisibilityChecker()
            .withFieldVisibility(JsonAutoDetect.Visibility.ANY)
            .withGetterVisibility(JsonAutoDetect.Visibility.NONE)
            .withSetterVisibility(JsonAutoDetect.Visibility.NONE)
            .withCreatorVisibility(JsonAutoDetect.Visibility.NONE);
Community
  • 1
  • 1
  • I don't know if it's helpful or not, but if you remove the annotation `@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "mimeType") ` from `AbstractBookmarkJsonModel` then you will have only one `mimeType` in your json – Katona Aug 14 '13 at 18:00

5 Answers5

72

I solved this by using the JsonTypeInfo.As.EXISTING_PROPERTY in the @JsonTypeInfo annotation.

The project is open source, check it out here: ANS.java

mergenchik
  • 1,129
  • 16
  • 27
Thomas Vaughan
  • 786
  • 7
  • 7
  • 5
    Good solution, with visible=true on @JsonTypeInfo (if not, the property is null after serialization) – c-toesca Apr 08 '16 at 20:34
  • It worked on "toJson()" but when I tried "fromJson()" the field was left with "null" value. Any thoughts? – ozma Dec 25 '16 at 13:07
  • 1
    `ANs.java` is broken. – Santosh Joshi Apr 10 '18 at 16:49
  • Looks like they switched the master branch over to node.js. Here's a link to the old ANS.java code that I originally linked: https://github.com/washingtonpost/ans-schema/blob/0.3.0/src/main/java/com/washingtonpost/arc/ans/v0_3/model/ANS.java – Thomas Vaughan Apr 11 '18 at 20:08
  • I tried this but whatever I do, my property comes as null if I say existing property. I guess because I don't set it - if you are going to set it manually, why bother adding it with the annotaitons at all? – Adam Jul 26 '19 at 14:50
  • This can't be a solution, as solutions don't rely on links that are so easily broken. Also, why would you post the corrected link in the comments rather than edit the answer? Please include the portions that are relevant from your link *inside your answer*. Indeed I would do it myself, but the file you link to doesn't include any instance of the string "existing" at all, so. – Fulluphigh Nov 23 '21 at 01:29
  • FYI, because his code isn't visible anymore, it'd look like: `@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "mimeType")` in this case. – Rick Hanton Mar 07 '22 at 21:15
12

I was having this exact same problem with the duplicate output. I found a solution that did not involve another property, and allowed me to not remove the original property. First, I set the visible flag to true for JsonTypeInfo. Then, I added a JsonIgnore annotation to the property declaration and the getter (but not the setter). This is so far outputting the JSON correctly with only one key for the type property.

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, visible = true, property = "mimeType")
@JsonSubTypes({
    @JsonSubTypes.Type(value = ImageBookmarkJsonModel.class, name = "image/jpeg"),
    @JsonSubTypes.Type(value = EpubBookmarkJsonModel.class, name = "application/epub+zip")
})
public abstract class AbstractBookmarkJsonModel extends AbstractJsonModel {
    @JsonIgnore
    @JsonProperty("mimeType")
    protected String mimeType;

    @JsonIgnore
    @JsonProperty("mimeType")
    public String getMimeType() {
        return mimeType;
    }

    @JsonProperty("mimeType")
    public void setMimeType(String mimeType) {
        this.mimeType = mimeType;
    }

}

To note, this is with fasterxml jackson jackson-databind 2.1.1

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.1.1</version>
</dependency>
ZacharyP
  • 730
  • 1
  • 7
  • 13
9

This behaviour is caused by the annotations placed on class AbstractBookmarkJsonModel:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "mimeType")
@JsonSubTypes({
    @JsonSubTypes.Type(value = ImageBookmarkJsonModel.class, name = "image/jpeg"),
    @JsonSubTypes.Type(value = EpubBookmarkJsonModel.class, name = "application/epub+zip")
})

@JsonTypeInfo tells Jackson to serialize the logical type name (JsonTypeInfo.Id.NAME) as a property (JsonTypeInfo.As.PROPERTY) with name mimeType (property = "mimeType"). With @JsonSubTypes.Type you assign the logical name application/epub+zip to EpubBookmarkJsonModel.

When it comes to serialization, Jackson serializes the logical name as a property mimeType = "application/epub+zip" then the properties of the object among them mimeType which happens to have the same value as the logical name application/epub+zip (assigned in the constructor).

I think mimeType should be changed to objectType in the @JsonTypeInfo annotation or even better to remove the mimeType field since Jackson will take care of that through type info serialization.

Katona
  • 4,816
  • 23
  • 27
  • 1
    +1 this is the right answer, Jackson adds an additional property to the serialized JSON output to identify the subtype. In your case you try to use an existing property, you should not do that. I know you cannot remove the `mimeType` property from the class because I think you need it. Simply use another subtype info property name like `subtypeName` and as subtype names use something like "`ImageBookmark`" and "`EpubBookmark`". This is the intended usage of Jacksons Polymorphic Type Handling. – Frank Olschewski Aug 22 '13 at 09:03
  • 3
    I don't consider this to be a sufficient answer, because this solution requires a user of Jackson to change their data model to accommodate some internal detail of how Jackson works (i.e. change the name of a field on a sub type). Also, at a high level I don't know why anyone would ever want duplicate keys in the output, so as far as I am concerned this is a bug! The EXISTING_PROPERTY solution works for us, fortunately. – Charles Capps Oct 25 '18 at 18:22
  • @CharlesCapps hm, may be you are right, however, I don't consider `mimeType` part of the data model, it's rather a _type flag_ which clearly exists only for the sake of JSON serialization. I don't think it would be there if the model wouldn't have to be serialized. – Katona Oct 25 '18 at 18:58
  • Yeah, I'm just talking generally speaking. In our case we often might have some enum on our data models that indicate the type of something. We shouldn't have to change our field names because some weird internal detail of Jackson causes duplicate fields to be emitted. Plus, I'd argue it's one logical field if a subclass implements an abstract method from a base class. – Charles Capps Oct 26 '18 at 21:22
1

This case probably just with legacy versions of jacson lib and it's simply resolved by moving yours @JsonProperty(value = "Your_CUSTOM_Name") from fields to getters.

Procrastinator
  • 2,526
  • 30
  • 27
  • 36
Fedulov Oleg
  • 67
  • 1
  • 4
1

Had the same problem. We're using Lombok, and I got this work with @JsonProperty access:

  @Getter
  @Setter
  @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) // Prevents duplication when serializing to JSON (subtype discriminator property)
  private RuleType ruleType;

I like this solution, since:

  • No dependency on another lib
  • Did not have to change my data model
  • Works with Lombok (less clutter)

And yes, need visible=true so that the property will get populated. In our case, it's an Enum with some behavior dispatching, so we certainly need it for our purposes, and it's great to be able to use it for Json subtype discrimination as well.

@JsonTypeInfo(
  use = JsonTypeInfo.Id.NAME,
  property = "ruleType",
  visible = true)
bmel
  • 21
  • 3