0

I think this can be better explained with an example(code is below). Suppose I have BaseClass and some sub-classes like Employee, Contractor etc. I do not want a database table for BaseClass(Hence using @MappedSuperClass). BaseClass has some common fields, but entityType is not a valid database field in any subclass like Employee, Contrator (Hence I denoted it as @Transient). Now I get back a json that has all the Employee values, it does not have endDate value that is in the BaseClass. I want to set the BaseClass's common fields like endDate in the Employee class and save it to the Employee table.

When retrieving I want to retrieve all Employees that have an endDate as some value. This should be possible because I did save the endDate when saving my Employee records.

Additional Info: I'm having trouble getting this to work. I'm using openAPI and I understood that I need this discriminator column to have a super class-sub class relationship generated - Inheritance from empty abstract class using openapi 3.0.

      @JsonIgnoreProperties(
  value = "entityType", // ignore manually set entityType, it will be automatically generated by Jackson during serialization
  allowSetters = true // allows the entityType to be set during deserialization
)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "entityType", visible = true)
@JsonSubTypes({
  @JsonSubTypes.Type(value = Employee.class, name = "Employee")
   @JsonSubTypes.Type(value = Contractor.class, name = "Contractor")
}) 
  @jakarta.persistence.MappedSuperclass
  public abstract BaseClass{
   //common to all sub-classes
   LocalDate endDate;

   /**
    Gets or Sets entityType
   */
   public enum EntityTypeEnum {
    Employee("Employee");
    Contractor("Contractor");

    private String value;

    EntityTypeEnum(String value) {
      this.value = value;
    }

    @JsonValue
    public String getValue() {
      return value;
    }

    @Override
    public String toString() {
      return String.valueOf(value);
    }

    @JsonCreator
    public static EntityTypeEnum fromValue(String value) {
      for (EntityTypeEnum b : EntityTypeEnum.values()) {
        if (b.value.equals(value)) {
          return b;
        }
      }
      throw new IllegalArgumentException("Unexpected value '" + value + "'");
    }
  }

     @JsonProperty("entityType")
     @jakarta.persistence.Transient
     private EntityTypeEnum entityType;

    }

    public Employee extends BaseClass {...}

    public Contractor extends BaseClass{...}

How would this deserializing work if I know that I'm de-serializing Employee.class, but the Json for employees that I receive does not have the entityType as a property at all? Currently I get 'missing type id property 'entityType'' I want to de-serialize a json of Employees to List class. The Json does not have entityType or any superclass(i.e. BaseClass from the above example) fields.

So to summarize, I want Jackson and JPA to play nicely with each other.

  1. I want to Jackson to correctly read a json, where the json has only the Employee related values(no superclass values yet) (At runtime I will know that I have Employee.class).
  2. I want to save this to the Employees table after setting my own values like endDate(which are in the BaseClass).
  3. I want to retrive this later, from the Employees table, knowing taht I have the endDate and Employee.class value at runtime.

Hope this makes sense. If not let me rephrase my questions: OpenAPI requires me to use the discriminator, in this case we call it entityType, for the inheritance to work when the classes are generated. But when I do that, Jackson complains that it cannot find the property id entityType when serializing a json. The json obviously does not have a field called entityType because we created this field. I tired variaous apporached including using : 1.

 objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE);

Adding the following to the top of the BaseClass:

@JsonIgnoreProperties(
  value = "entityType", // ignore manually set entityType, it will be automatically generated by Jackson during serialization
  allowSetters = true // allows the entityType to be set during deserialization
)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "entityType")

and I still get this error com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve subtype of [simple type, class Employee]: missing type id property 'entityType'

user2441441
  • 1,237
  • 4
  • 24
  • 45

1 Answers1

0

How would this deserializing work if I know that I'm de-serializing Employee.class, but the Json for employees that I receive does not have the entityType as a property at all?

If you know that the JSON represents an Employee and not a Contractor (e.g. based on the source) then you can invoke the mapper passing the Employee class. But if you are using this in a general case where you want the deserialization to return either Employee or Contractor then there needs to be a discriminator so the deserializer can choose the correct concrete class itself. (Consider the case where two subclasses have identical fields, but nevertheless are being modelled as different classes - there would be no way for the serializer to guess which class it should instantiate.)

(1) I suggest that you accept the permanent addition of entityType in the superclass - and in particular you should serialize it so that the JSON then carries the type info. And (2) you should remove the @JsonIgnoreProperties... so that the type info is available to the deserializer.

To enforce the correct type automatically here's what you can do: (3) BaseClass should require entityType in its constructor and you can code the correct type in Employee and Contractor constructors so they pass the type to BaseClass.

As you are now managing the entityType yourself, I think (4) the @JsonTypeInfo should be:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "entityType", visible = true)
                                                                    ^^^^^^^^^

This approach is compatible with JPA. I am using Jackson, Spring Data and Mongo as my backing store. In the case of Mongo, Spring adds a _class field as its own different way to handle the de/serialisation. The _class field occurs in the database but is not in the java object. So this approach is using two parallel methods of type management and they do not collide; in fact this allows finer-grain control should you need slightly different external (JSON) models compared to the backing store.

AndrewL
  • 2,034
  • 18
  • 18
  • 'I suggest that you accept the permanent addition of entityType in the superclass' - The problem is that the json is returned by an external API and I cannot modify it. – user2441441 May 22 '23 at 17:57
  • OK, so how can you/the machine distinguish a `Employee` from a `Contractor`? Can provide some specimen JSON for both? There is another way... – AndrewL May 23 '23 at 01:03
  • OR to you always know at the point you invoke the deserialization what the content is: Employee or Contractor? – AndrewL May 23 '23 at 01:13
  • Yes I do know at the point of de-serialization what content it is, because the generic class gets the class type of the expected json as well. I can remove entityType, but then the openAPI doesn't generate the class hierarchy without the discriminator. I guess I can keep the BaseClass outside the OpenAPI somehow. – user2441441 May 23 '23 at 14:25
  • 'then you can invoke the mapper passing the Employee class. -> how to do that? I tired TypeReference> typeReference= new TypeReference<>(){}; objectMapper.readValue(new File(...), typeReference); – user2441441 May 23 '23 at 22:34