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

polymorph = true Enables polymorphic persistence. When set on @Entity, Morphium stores a class_name discriminator field in each document so it can reconstruct the correct Java subtype on read. Must be set on EVERY class in the hierarchy. MongoDBAtlasCosmosDB typeId (class_name) The discriminator field automatically stored by Morphium when polymorph = true. Contains the fully-qualified Java class name (e.g. "io.quarkiverse.morphium.showcase.polymorphism.entity.Car"). Used during deserialization to pick the correct class. MongoDBAtlasCosmosDB @Entity(collectionName) Maps a Java class to a specific MongoDB collection. In a polymorphic hierarchy, ALL classes must declare the SAME collectionName so they share one collection. Import: de.caluga.morphium.annotations.Entity MongoDBAtlasCosmosDB MorphiumTypeMapper Interface for custom serialization/deserialization of arbitrary Java types. Implement marshall() to convert Java to BSON and unmarshall() to convert BSON back to Java. Registered automatically via CDI in Quarkus. Import: de.caluga.morphium.objectmapping.MorphiumTypeMapper MongoDBAtlasCosmosDB @FieldNameConstants Lombok annotation (not Morphium!). Generates Vehicle.Fields.manufacturer, Car.Fields.doors etc. at compile time for type-safe queries. Import: lombok.experimental.FieldNameConstants MongoDBAtlasCosmosDB storeList (batch) Bulk-inserts multiple entities in a single MongoDB write operation. Much more efficient than individual store() calls. Morphium sends a single bulkWrite command. Usage: morphium.storeList(vehicleList) MongoDBAtlasCosmosDB

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 = true on every class — this annotation must appear on the base class AND every subclass. It tells Morphium to write and read the class_name discriminator field.
  • class_name discriminator 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 converts Money to a simple Map for storage and back on read. In Quarkus, annotating the mapper with @ApplicationScoped auto-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 = true
2 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