Search code examples
javajsonhibernatejacksontapestry

Tapestry JPA Jackson Deserialization


I'm working on a project I didn't initially create, in which the data was stored in-memory. I'm curently moving this data into the database. I'm doing this using hibernate and tapestry JPA. At some point in the project Jackson Deserialization is used (actually in connection with a UI, but I doubt that's relevant), via the @JsonDeserialize annotation, with a deserializer class (let's call it DefinitionDeserializer). DefinitionDeserializer then creates an instance of a POJO representation (let's call it Definition) of a database table (D_DEFINITION). However, D_DEFINITION has a connection to another table (D_TYPE) (and hence another POJO (PeriodType)). To resolve this connection, I'm using a tapestry service (ConnectingService), which I usually inject via the @Inject annotation. However, I can't use this method of injection when the object (in which I'm trying to inject the service, i.e. DefinitionDeserializer) was created via the new keyword - which seems to be the case for the @JsonDeserialize annotation. I also can't use ConnectingService without injecting it via the @Inject keyword, because then I couldn't inject any other services in ConnectingService either, which I'm currently doing.

I'm hoping this description didn't confuse you too much, I can't share the actual code with you and I don't think a minimal example would be much better, as it's quite a complicated case and wouldn't be such a small piece of code. If you need one, however, I can try to provide one.

Basically what I need is a way to tell JsonDeserialize to take a tapestry service instead of creating an instance of DefinitionDeserializer itself.

Edit: The classes as examples:

public DefinitionDeserializer extends StdDeserializer<Definition> {
    private static final long serialVersionUID = 1L;
    //TODO: The injection doesn't work yet
    @Inject
    private ConnectingService connectingService;

    public DefinitionDeserializer() {
        this(null);
    }

    public DefinitionDeserializer(Class<?> vc) {
        super(vc);
    }

    @Override
    public Definition deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        Definition pd = new Definition();
        JsonNode node = p.getCodec().readTree(p);
        if (node.has("type"))
            pd.setType(periodTypeDao.findByValue("PeriodType." + node.get("type").asText()));

        return pd;
    }

}

@Entity
@Table(name = Definition.TABLE_NAME)
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE, region =
        JpaEntityModelConstants.CACHE_REGION_ADMINISTRATION)
public class Definition {

    public static final String TABLE_NAME = "D_DEFINITION";
    private static final long serialVersionUID = 389511526676381027L;

    @Id
    @SequenceGenerator(name = JpaEntityModelConstants.SEQUENCE_NAME, sequenceName = JpaEntityModelConstants.SEQUENCE_NAME, initialValue = 1, allocationSize = 1)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = JpaEntityModelConstants.SEQUENCE_NAME)
    @Column(name = "ID")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumns({
            @JoinColumn(name = "FK_TYPE", referencedColumnName = "ID")}
    )
    private PeriodType type;


    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public PeriodType getType() {
        return type;
    }

    public void setType(PeriodType dpmType) {
        this.type = dpmType;
    }

    //More columns
}

PeriodType looks pretty much the same as Definition.


//BaseService contains all the standard methods for tapestry JPA services
public interface ConnectingService extends BaseService<PeriodType> {

}

public class ConnectingServiceImpl extends BaseServiceImpl<PeriodType> implements ConnectingService {

    public ConnectingServiceImpl() {
        super (PeriodType.class);
    }
}

Currently I'm using it like this (which doesn't work):

@JsonDeserialize(using = DefinitionDeserializer.class)
@JsonSerialize(using = DefinitionSerializer.class)
private Definition definition;

Solution

  • @JsonDeserialize doesn't create instances of deserialisers, it's just a hint for ObjectMapper to know which class to use when deserialising.

    By default ObjectMapper uses Class.newInstance() for instantiating deserialisers, but you can specify custom HandlerInstantiator (ObjectMapper#setHandlerInstantiator()) in which you can use Tapestry's ObjectLocator to get instances of deserialisers, i.e. using ObjectLocator#autobuild(), or use ObjectLocator#getService() if your deserialisers are Tapestry services themselves.

    Update:

    public class MyHandlerInstantiator extends HandlerInstantiator
    {
        private final ObjectLocator objectLocator;
    
        public MyHandlerInstantiator(ObjectLocator objectLocator)
        {
            this.objectLocator = objectLocator;
        }
    
        @Override
        public JsonDeserializer<?> deserializerInstance(
         DeserializationConfig config, Annotated annotated, Class<?> deserClass)
        {
            // If null returned here instance will be created via reflection
            // you can always use objectLocator, or use it conditionally
            // just for some classes
            return objectLocator.autobuild(deserClass);
        }
    
        // Other method overrides can return null
    }
    

    then later when you're configuring ObjectMapper use @Injected instance of ObjectLocator to create an instance of custom HandlerInstantiator, i.e.:

    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.setHandlerInstantiator(new MyHandlerInstantiator(objectLocator));
    return objectMapper;