Polymorphism / Custom Mappers
Type Hierarchies, typeId Discriminator, MorphiumTypeMapper
What Morphium Offers
Morphium handles Java type hierarchies transparently. A type discriminator (class_name
or custom typeId) is stored with each document, and Morphium deserializes to the correct
subclass automatically. Custom TypeMapper implementations give full control over
discriminator values.
The Challenge
Polymorphic document storage typically requires custom deserializers, manual type registration and careful handling of schema evolution. Most ODMs either don't support it or make it painful.
Morphium Features Used
Prerequisites & Key Concepts
- All subclasses must have the same
collectionName— Vehicle, Car, and Truck all declare@Entity(collectionName = "vehicles", polymorph = true). Without this, each class would map to its own collection and polymorphic queries would fail. polymorph = trueon every class — this annotation must appear on the base class AND every subclass. It tells Morphium to write and read theclass_namediscriminator field.class_namediscriminator is stored automatically in each document. It contains the fully-qualified Java class name. On read, Morphium uses this field to instantiate the correct subtype.MorphiumTypeMapper<Money>— for custom value types that Morphium does not know how to serialize by default, you implement a type mapper. The mapper convertsMoneyto a simpleMapfor storage and back on read. In Quarkus, annotating the mapper with@ApplicationScopedauto-registers it.
Entity Relationship
Vehicle (@Entity, polymorph)
_id: MorphiumId @Id
manufacturer: String
model: String
year: int
price: double
class_name: String auto (discriminator)
extends →
Car (@Entity, polymorph)
doors: int
fuelType: String
convertible: boolean
extends →
Truck (@Entity, polymorph)
payloadTons: double
axles: int
hasTowbar: boolean
@Entity
@Embedded
@Reference
Entity Source Code
Vehicle.java (Base Class)
Java
import de.caluga.morphium.annotations.Entity; import de.caluga.morphium.annotations.Id; import de.caluga.morphium.driver.MorphiumId; import lombok.Data; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; import lombok.experimental.FieldNameConstants; @Entity(collectionName = "vehicles", polymorph = true)1 @Data @NoArgsConstructor @AllArgsConstructor @FieldNameConstants public class Vehicle { @Id private MorphiumId id; private String manufacturer; private String model; private int year; private double price; public String getTypeName() {2 return getClass().getSimpleName(); } }
1
polymorph = true stores a class_name discriminator field so Morphium can reconstruct the correct Java subtype on read. ALL classes in the hierarchy must use the same collectionName.2 Returns the actual runtime type name (e.g. "Car", "Truck") — proves polymorphic deserialization works.
Car.java (Subclass)
Java
import de.caluga.morphium.annotations.Entity; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; import lombok.experimental.FieldNameConstants; @Entity(collectionName = "vehicles", polymorph = true)1 @Data @EqualsAndHashCode(callSuper = true) @NoArgsConstructor @AllArgsConstructor @FieldNameConstants public class Car extends Vehicle {2 private int doors; private String fuelType; private boolean convertible; }
1 Must repeat
@Entity with the SAME collectionName and polymorph = true2 Extends Vehicle — subclass fields are stored alongside inherited fields in the same document
Truck.java (Subclass)
Java
import de.caluga.morphium.annotations.Entity; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; import lombok.experimental.FieldNameConstants; @Entity(collectionName = "vehicles", polymorph = true) @Data @EqualsAndHashCode(callSuper = true) @NoArgsConstructor @AllArgsConstructor @FieldNameConstants public class Truck extends Vehicle {1 private double payloadTons; private int axles; private boolean hasTowbar; }
1 Another subclass sharing the same
"vehicles" collection — same pattern as Car with truck-specific fields.Custom Type Mapper
MoneyMapper.java
Java
import de.caluga.morphium.objectmapping.MorphiumTypeMapper; import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped1 public class MoneyMapper implements MorphiumTypeMapper<Money> {2 @Override public Object marshall(Money o) {3 // Convert Money to a Map for MongoDB storage return Map.of( "amount", o.getAmount(), "currency", o.getCurrency() ); } @Override public Money unmarshall(Object d) {4 // Reconstruct Money from the Map read from MongoDB if (d instanceof Map<?, ?> map) { double amount = ((Number) map.get("amount")).doubleValue(); String currency = (String) map.get("currency"); return new Money(amount, currency); } throw new IllegalArgumentException("Cannot unmarshall Money from: " + d); } }
1
@ApplicationScoped makes this a CDI bean — Quarkus Morphium auto-discovers and registers it as the type mapper for Money.2
implements MorphiumTypeMapper<Money> tells Morphium which Java type this mapper handles; the generic parameter is the key for registration.3
marshall() converts the Java object to a BSON-compatible value (here a Map) for storage in MongoDB.4
unmarshall() reconstructs the Java object from the raw BSON value read back from MongoDB.Service Code
Morphium API — Polymorphic Query
Java
import de.caluga.morphium.Morphium; @Inject Morphium morphium; // Query base class — returns mixed Car/Truck list List<Vehicle> all = morphium.createQueryFor( Vehicle.class).asList(); // Query specific subclass List<Car> cars = morphium.createQueryFor( Car.class).asList(); // Store mixed types morphium.storeList(List.of(car, truck));
Related Documentation
- Developer Guide — polymorph=true, typeId, Type Hierarchies
- API Reference — createQueryFor(), store(), dropCollection()