Skip to content

Custom serializers

Mirko Da Corte edited this page Aug 25, 2026 · 8 revisions

Scrinium ships serializers for common shapes and lets you register your own for value types. You attach a serializer in one of two places:

  • On a member, inside a model map: map.GetMemberMap(x => x.Member).SetSerializer(...).
  • On a type, engine-wide: dbContextEngine.MapRegistry.AddCustomSerializerMap(new MySerializer()).

Built-in serializers

All live in Etherna.Scrinium.Core.Serialization.Serializers.

Serializer For Notes
EnumerableSerializer<TItem> IEnumerable<TItem> Wrap a child item serializer — e.g. a collection of references.
DictionarySerializer<TKey, TValue> IDictionary<TKey, TValue> Configurable representation and key/value serializers.
ReadOnlyDictionarySerializer<TKey, TValue> IReadOnlyDictionary<TKey, TValue> As above, read-only.
HexToBinaryDataSerializer string (hex) Stores a hex string as BSON Binary — compact for hashes/addresses.
GeoPointSerializer<TInModel> a model with lon/lat members Stores it as a GeoJSON Point for geospatial indexes/queries.
ExtraElementsSerializer object Migration helper: read legacy values out of ExtraElements.
ReferenceSerializer<TModelBase, TKey> an entity reference Denormalized summary + lazy load — see References and denormalization.

EnumerableSerializer

Wrap another serializer to serialize a collection of it. The key use is a collection of references:

map.GetMemberMap(t => t.Members)
   .SetSerializer(new EnumerableSerializer<User>(UserReference(dbContextEngine)));

With no argument it serializes items with their default serializer.

Dictionary serializers

map.GetMemberMap(x => x.Labels)
   .SetSerializer(new DictionarySerializer<string, string>(DictionaryRepresentation.Document));

Pass key/value serializers to the three-arg constructor when the values need special handling (e.g. a dictionary whose values are references).

HexToBinaryDataSerializer

Store a hex string (a hash, an address) as binary instead of text:

map.GetMemberMap(x => x.Hash).SetSerializer(new HexToBinaryDataSerializer());

GeoPointSerializer

Serialize a model's longitude/latitude pair as a GeoJSON point (enabling 2dsphere queries):

map.GetMemberMap(x => x.Location)
   .SetSerializer(new GeoPointSerializer<Location>(dbContextEngine, l => l.Longitude, l => l.Latitude));

ExtraElementsSerializer

A helper for schema fix functions and migrations: pull a typed value out of the raw ExtraElements bag of a legacy document.

var serializer = new ExtraElementsSerializer(dbContextEngine);
var legacy = serializer.DeserializeValue<LegacyShape>(model.ExtraElements!["oldField"]);

Writing your own

Derive from the driver's SerializerBase<T> and register it type-wide with AddCustomSerializerMap. A common case is representing a value differently on disk — money as Decimal128, an encrypted string, a domain primitive:

public sealed class MoneySerializer : SerializerBase<decimal>
{
    private readonly IBsonSerializer<decimal> inner = new DecimalSerializer(BsonType.Decimal128);

    public override decimal Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) =>
        inner.Deserialize(context, args);

    public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, decimal value) =>
        inner.Serialize(context, args, value);
}
class MoneyMap : IModelMapsCollector
{
    public void Register(IDbContextEngine dbContextEngine) =>
        dbContextEngine.MapRegistry.AddCustomSerializerMap(new MoneySerializer());
}

Members typed object

A member typed object — or an object valued dictionary, the usual metadata bag — serializes through the driver's ObjectSerializer, which accepts only the types of its allow list: the driver's default framework types — primitives, common value types, framework collections, and arrays and enums of allowed types — and nothing else by default. A document naming any other type in its _t is refused on read, and a value of another type is refused on write, so a document can't decide which type materializes in your model.

Widen it deliberately, for the types you accept, by registering an ObjectSerializer of your own:

using Etherna.MongoDB.Bson.Serialization.Serializers;

dbContextEngine.MapRegistry.AddCustomSerializerMap<object>(
    new ObjectSerializer(type => ObjectSerializer.DefaultAllowedTypes(type) ||
                                 type == typeof(MyAllowedType)));

The registered ObjectSerializer is also what makes interface typed members work: the driver's interface serializer installs itself only over it and writes the member through it, so only allowed implementations persist; on read the implementation is resolved from the document's discriminator.

Note. An entity model never belongs in an object member: entities are stored as references, and the allow list refuses a mapped model type in an object member, on write and on read — never widen it with an entity type. (The engine-build freeze validates declared member serializers only, so it can't see what an object member holds — see References and denormalization for the freeze.)

An id typed object is a different matter, and is refused at engine build: it doesn't commit to an id type, the contract repositories and references key on. Registering a custom serializer map for object, as above, is what declares how those values serialize and deserialize, and makes such an id accepted — its rendered value must still be a value, never a document (Domain models).

Custom serialized entity ids

A custom serialized type can also key an entity — be the TKey of an IEntityModel<TKey>. Repositories, references and the identity map address the entity with the typed value, and the document stores _id in the custom representation. A domain primitive as id:

public readonly struct Isbn(string value)
{
    public string Value { get; } = value;
}

public sealed class IsbnSerializer : SerializerBase<Isbn>
{
    public override Isbn Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) =>
        new(context.Reader.ReadString());

    public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, Isbn value) =>
        context.Writer.WriteString(value.Value);
}

public class Book : EntityModelBase<Isbn>
{
    public Book(Isbn isbn, string title)
    {
        Id = isbn;
        Title = title;
    }
    protected Book() { }

    public virtual string Title { get; set; }
}
class BookMap : IModelMapsCollector
{
    public void Register(IDbContextEngine dbContextEngine)
    {
        dbContextEngine.MapRegistry.AddCustomSerializerMap(new IsbnSerializer());

        dbContextEngine.MapRegistry.AddModelMap<EntityModelBase<Isbn>>(
            "0e18ba21-6b7c-4e5c-9e0a-3d2f41c88b57");
        dbContextEngine.MapRegistry.AddModelMap<Book>(
            "5a7d90cf-2143-48f2-a5b8-6c1e0d9b374a");
    }
}

An Isbn id has no id generator: the application assigns it before insert (Id = isbn above). The same serializer serves every other Isbn member of the db context, and the type works in queries and indexes like any other member type.

An id serializer writes a value. Whatever the id type, its serializer must emit a BSON value — the WriteString above — never a document or an array (the rationale is on Domain models). A serializer emitting a document is refused when it renders, by every operation addressing a document by its key and by the create itself; the id types whose serializer declares a composite fail fast at engine build.

Note. AddCustomSerializerMap claims the serializer of its type when invoked: register custom serializer maps before the model maps of the entities they key, like BookMap above. Mapping the entity resolves the id type's serializer immediately; for a type the driver otherwise serves with a default serializer (like Guid below), a claim arriving after that resolution conflicts with it and fails at startup with a BsonSerializationException.

Guid ids

The driver's default Guid serializer has an unspecified representation and fails at use: to key entities with Guid, claim the representation with a custom serializer map. The driver id generator assigns the value on insert.

dbContextEngine.MapRegistry.AddCustomSerializerMap(new GuidSerializer(GuidRepresentation.Standard));

dbContextEngine.MapRegistry.AddModelMap<EntityModelBase<Guid>>(
    "c9f2e8d4-7b16-45a3-9c58-0e6a2d31f7b9");

Next: Model mapping for attaching serializers to members, References and denormalization for reference serializers, or Indexes for geospatial and other indexes.

Clone this wiki locally