spring-data-mongodb icon indicating copy to clipboard operation
spring-data-mongodb copied to clipboard

Deserializing enums as interfaces with Spring Data MongoDB [DATAMONGO-1884]

Open spring-projects-issues opened this issue 7 years ago • 2 comments

Joaquin Peñalver opened DATAMONGO-1884 and commented

I've currently a problem with serialize/deserialize process in MongoDB of an object that contains an attribute defined by a marker interface and the implementations of this interface are Enums.

My used versions are Spring Boot 1.5.2, Spring 4.3.7 and Spring-data-mongodb 1.10.1.

The piece of code afected is:

public interface EventType {
    String getName(); 
}
public interface DomainEvent extends Serializable {

    UUID getId();    
    LocalDateTime getOccurredOn();    
    EventType getEventType();    
    String getEventName();    
}
public abstract class AbstractDomainEvent implements DomainEvent {

    private UUID id;
    private LocalDateTime occurredOn;
    private EventType eventType;

    protected AbstractDomainEvent(EventType eventType) {
        this.id = UUID.randomUUID();
        this.occurredOn = LocalDateTime.now();
        this.eventType = eventType;
    }
}
public class MyEventOne extends AbstractDomainEvent {

    private Object myConcreteData;

    public MyEventOne(Object data) {

        super(MyEventType.EVENT_ONE);
        this.myConcreteData = data;
    }    
}
public enum MyEventType implements EventType {

    EVENT_ONE,
    EVENT_N;

    @Override
    public String getName() {
        return this.name();
    }    
}

Ok, well. My problem is when I try to deserialize an event persisted in mongoDB.

When I persist MyEventOne, Spring data mongo persist the object as:

{
    "_class" : "xxx.xxx.xxx.MyEventOne",
    "_id" : LUUID("d74478e7-258c-52c4-4fc5-aba20a30d4b6"),
    "occurredOn" : ISODate("2018-02-21T14:39:53.549Z"),
    "eventType" : "EVENT_ONE"
}

Note the eventType is serialized as String, I know that String is the default representation of a Enum (real class), but in this case is referenced as a interface EventType in AbstractDomainEvent.

When I try to read this document, I have this exception:

org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [java.lang.String] to type [xxx.xxx.xxx.EventType]

Any idea? May be it's a bug? May be spring-data-mongo should resolve this situation inserting a metadata information about the concrete Enum instance, like a "_class" field?

I try insert @JsonTypeInfo annotation in EventType attribute at AbstractDomainEvent but it doesnt works, i think that it's ignored.

The temporary solution might be write a custom converter, but i think that this feature can be a framework feature.

Thanks in advcance!


Reference URL: https://stackoverflow.com/questions/48910761/deserializing-interfaces-with-spring-data-mongodb

spring-projects-issues avatar Feb 22 '18 10:02 spring-projects-issues

Oliver Drotbohm commented

It is a bug indeed but a tricky one to solve. We currently write enums as their names as thats the most efficient way to store them. However you should be able to register a Converter<MyEventType, DBObject> to make sure the enum values get written as nested documents and a Converter<DBObject, EventType> to then read the written structure again and produce enum values based on the type information you wrote.

It's a bit hard to suggest a perfect way to solve that as your setup indicates that there could be other implementations of EventType as well and especially on the reading side you might already need to inspect the source value to decide which of the converters to actually use for reading. Because of those specifics, I'm not quite sure we can / should be actually change our default behavior

spring-projects-issues avatar Feb 22 '18 11:02 spring-projects-issues

I have hit this problem too.

Enum

public enum MyEnum {

	TITLE("title");

	private String value = "";

	private static final Map<String, MyEnum> BY_VALUE = new HashMap<>();

	static {
		for (MyEnum e : values()) {
			BY_VALUE.put(e.value, e);
		}
	}

	private MyEnum(String value) {
		this.value = value;
	}

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

	public static MyEnum byValue(String val) {
		return BY_VALUE.get(val);
	}
}

Domain class

@Data
@Document(collection = "stuff")
public class MongoStuff {
	private Map<MyEnum, String> props = new HashMap<>();
}

Repository

public interface MyStuffRepository extends MongoRepository<MongoStuff, ObjectId> {}

Code - write

MongoStuff ms = new MongoStuff();
ms.getProps().put(MyEnum.TITLE, "Arrrrgghhh!");
myStuffRepository.save(ms);

Creates:

{ _id: ObjectId(1234...), "props": { "title": "Arrrrgghhh!" }}

Code - read

ms = myStuffRepository.findById(ObjectId(1234..)).orElseThrow(....);

but it blows up with message in reporter's comment.

It seems you are using Enum.toString() to write and not Enum.name() (which it probably should do for conversion consistancy) because the toString value is in the db as the Map key and not the name() value of "TITLE".

I have added Converter<MyEnum, String> and Converter<String, MyEnum> and tried with each one separately and both together and it either prevents queries on other collections (fields become null when they get to the db level) or it defaults to the Standard Enum converter which blows up.

To me it looked like it was mostly working because of the toString/name inconsistancy but then the Converters didn't work either I had to start Googling and here I am.

I have other Enums in my domain classes and that all works fine but having the Enum be part of a Map doesn't work.

The default implementation 50% works, prehaps you could inject a byValue and get the other 50% :) or better still get the converters to honour my enum types instead of the base Enum type.

davidnewcomb avatar Dec 13 '24 04:12 davidnewcomb