Json Serialization
An absolutely essential ingredient in Marten's persistence strategy is JSON serialization of the document objects. Marten aims to make the JSON serialization extensible and configurable through the native mechanisms in each JSON serialization library.
Serializer Choice
Marten ships with implementations for both Newtonsoft.Json & System.Text.Json. Newtonsoft.Json is enabled by default in Marten for backwards compatibility with previous Marten versions and because it handles some unique edge-cases that System.Text.Json might not.
That being said, if you're working on a new application we recommend enabling System.Text.Json for improved performance and serializer alignment with ASP.NET Core & Wolverine defaults.
Configuration for both serializers hang off the DocumentStore
UseNewtonsoftForSerialization
and UseSystemTextJsonForSerialization
extensions respectively:
var store = DocumentStore.For(_ =>
{
_.Connection("some connection string");
// Newtonsoft - Enabled by default
_.UseNewtonsoftForSerialization();
// System.Text.Json - Opt in
_.UseSystemTextJsonForSerialization();
});
Enum Storage
Marten allows you to configure how enum values are being stored. By default, they are stored as integers but it is possible to change that to strings:
var store = DocumentStore.For(_ =>
{
// Newtonsoft
_.UseNewtonsoftForSerialization(enumStorage: EnumStorage.AsString);
// STJ
_.UseSystemTextJsonForSerialization(enumStorage: EnumStorage.AsString);
});
Field Names Casing
By default, Marten stores field names "as they are" (C# naming convention is PascalCase for public properties).
You can have them also automatically formatted to:
CamelCase
,snake_case
by changing the relevant serializer settings:
var store = DocumentStore.For(_ =>
{
// Newtonsoft
_.UseNewtonsoftForSerialization(casing: Casing.CamelCase);
// STJ
_.UseSystemTextJsonForSerialization(casing: Casing.CamelCase);
});
Non Public Members Storage
By default Newtonsoft.Json
only deserializes properties with public setters.
You can allow deserialization of properties with non-public setters by changing the serialization settings in the DocumentStore
options.
var store = DocumentStore.For(_ =>
{
// Allow the JsonNetSerializer to also deserialize using non-public setters
_.UseNewtonsoftForSerialization(nonPublicMembersStorage: NonPublicMembersStorage.NonPublicSetters);
});
You can also use other options of NonPublicMembersStorage
:
NonPublicDefaultConstructor
- allows deserialization using non-public default constructor,NonPublicConstructor
- allows deserialization using any constructor. Construction resolution uses the following precedence:- Constructor with
JsonConstructor
attribute. - Constructor with the biggest parameters' count.
- If two constructors have the same parameters' count, use public or take the first one.
- Use default constructor.
- Constructor with
All
- Use both properties with non-public setters and non-public constructors.
When using System.Text.Json
the only support for private properties is to mark the field using [JsonInclude] attribute.
Alternatively if you want immutability you can mark the setter as init
like so:
public class User
{
public int Id { get; init; }
}
Collection Storage Newtonsoft Only
Marten by default stores the collections as strongly typed (so with $type and $value). Because of that and current MartenQueryable
limitations, it might result in not properly resolved nested collections queries.
Changing the collection storage to AsArray
using a custom JsonConverter
will store it as regular JSON array for the following:
ICollection<>
,IList<>
,IReadOnlyCollection<>
,IEnumerable<>
.
That improves the nested collections queries handling.
To do that you need to change the serialization settings in the DocumentStore
options.
var store = DocumentStore.For(_ =>
{
// Replace the default (strongly typed) JsonNetSerializer collection storage
// with JSON array formatting
_.UseNewtonsoftForSerialization(collectionStorage: CollectionStorage.AsArray);
});
Custom Configuration
Marten also allows you to completely override all serializer settings via the last configuration parameter:
var store = DocumentStore.For(_ =>
{
_.UseNewtonsoftForSerialization(
enumStorage: EnumStorage.AsString,
configure: settings =>
{
settings.DateTimeZoneHandling = DateTimeZoneHandling.RoundtripKind;
settings.ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor;
});
_.UseSystemTextJsonForSerialization(
enumStorage: EnumStorage.AsString,
configure: settings =>
{
settings.MaxDepth = 100;
});
});
WARNING
You should not override the Newtonsoft.Json ContractResolver
with CamelCasePropertyNamesContractResolver
for Json Serialization. Newtonsoft.Json by default respects the casing used in property / field names which is typically PascalCase. This can be overridden to serialize the names to camelCase and Marten will store the JSON in the database as specified by the Newtonsoft.Json settings. However, Marten uses the property / field names casing for its SQL queries and queries are case sensitive and as such, querying will not work correctly.
Marten actually has to keep two Newtonsoft.Json serializers, with one being a "clean" Json serializer that omits all Type metadata. The need for two serializers is why the customization is done with a nested closure so that the same configuration is always applied to both internal JsonSerializer's
.
External Configuration
You might prefer to configure the serializer separately from the document store and can do so via passing the serializer instance to the Serializer
method.
An example of configuring Marten's JsonNetSerializer
is shown below:
var serializer = new Marten.Services.JsonNetSerializer();
// To change the enum storage policy to store Enum's as strings:
serializer.EnumStorage = EnumStorage.AsString;
// All other customizations:
serializer.Configure(_ =>
{
// Code directly against a Newtonsoft.Json JsonSerializer
_.DateTimeZoneHandling = DateTimeZoneHandling.RoundtripKind;
_.ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor;
});
var store = DocumentStore.For(_ =>
{
// Replace the default JsonNetSerializer with the one we configured
// above
_.Serializer(serializer);
});
Integrating a Custom serializer
Internally, Marten uses an adapter interface for JSON serialization:
/// <summary>
/// When selecting data through Linq Select() transforms,
/// should the data elements returned from Postgresql be
/// cast to their raw types or simple strings
/// </summary>
public enum ValueCasting
{
/// <summary>
/// Json fields will be returned with their values cast to
/// the proper type. I.e., {"number": 1}
/// </summary>
Strict,
/// <summary>
/// Json fields will be returned with their values in simple
/// string values. I.e., {"number": "1"}
/// </summary>
Relaxed
}
public interface ISerializer
{
/// <summary>
/// Just gotta tell Marten if enum's are stored
/// as int's or string's in the JSON
/// </summary>
EnumStorage EnumStorage { get; }
/// <summary>
/// Specify whether properties in the JSON document should use Camel or Pascal casing.
/// </summary>
Casing Casing { get; }
/// <summary>
/// Controls how the Linq Select() behavior needs to work in the database
/// </summary>
ValueCasting ValueCasting { get; }
/// <summary>
/// Serialize the document object into a JSON string
/// </summary>
/// <param name="document"></param>
/// <returns></returns>
string ToJson(object? document);
/// <summary>
/// Deserialize a JSON string stream into an object of type T
/// </summary>
T FromJson<T>(Stream stream);
/// <summary>
/// Deserialize a JSON string into an object of type T
/// </summary>
T FromJson<T>(DbDataReader reader, int index);
/// <summary>
/// Deserialize a JSON string stream into an object of type T
/// </summary>
ValueTask<T> FromJsonAsync<T>(Stream stream, CancellationToken cancellationToken = default);
/// <summary>
/// Deserialize a JSON string into an object of type T
/// </summary>
ValueTask<T> FromJsonAsync<T>(DbDataReader reader, int index, CancellationToken cancellationToken = default);
/// <summary>
/// Deserialize a JSON string stream into an object of type T
/// </summary>
object FromJson(Type type, Stream stream);
/// <summary>
/// Deserialize a JSON string into the supplied Type
/// </summary>
object FromJson(Type type, DbDataReader reader, int index);
/// <summary>
/// Deserialize a JSON string stream into an object of type T
/// </summary>
ValueTask<object> FromJsonAsync(Type type, Stream stream, CancellationToken cancellationToken = default);
/// <summary>
/// Deserialize a JSON string into the supplied Type
/// </summary>
ValueTask<object> FromJsonAsync(Type type, DbDataReader reader, int index,
CancellationToken cancellationToken = default);
/// <summary>
/// Serialize a document without any extra
/// type handling metadata
/// </summary>
/// <param name="document"></param>
/// <returns></returns>
string ToCleanJson(object? document);
/// <summary>
/// Write the JSON for a document with embedded
/// type information. This is used inside the patching API
/// to handle polymorphic collections
/// </summary>
/// <param name="document"></param>
/// <returns></returns>
string ToJsonWithTypes(object document);
}
To support a new serialization library or customize the JSON serialization options, you can write a new version of ISerializer
and plug it into the DocumentStore
. An example of integrating with the Jil
serializer is below.
TIP
Regardless of which JSON serializer you use, make sure to set the Casing
property on the Marten ISerializer
interface instead of directly overriding the member naming on the underlying JSON serializer. The Linq querying support needs this information in order to create the correct SQL queries within JSON bodies.
WARNING
The code below is provided as an example only, Jil
is no longer maintained and should not be used in your application.
public class JilSerializer : ISerializer
{
private readonly Options _options
= new(dateFormat: DateTimeFormat.ISO8601, includeInherited:true);
public ValueCasting ValueCasting { get; } = ValueCasting.Strict;
public string ToJson(object? document)
{
return JSON.Serialize(document, _options);
}
public T FromJson<T>(Stream stream)
{
return JSON.Deserialize<T>(stream.GetStreamReader(), _options);
}
public T FromJson<T>(DbDataReader reader, int index)
{
var stream = reader.GetStream(index);
return FromJson<T>(stream);
}
public ValueTask<T> FromJsonAsync<T>(Stream stream, CancellationToken cancellationToken = default)
{
return new(FromJson<T>(stream));
}
public ValueTask<T> FromJsonAsync<T>(DbDataReader reader, int index, CancellationToken cancellationToken = default)
{
return new (FromJson<T>(reader, index));
}
public object FromJson(Type type, Stream stream)
{
return JSON.Deserialize(stream.GetStreamReader(), type, _options);
}
public object FromJson(Type type, DbDataReader reader, int index)
{
var stream = reader.GetStream(index);
return FromJson(type, stream);
}
public ValueTask<object> FromJsonAsync(Type type, Stream stream, CancellationToken cancellationToken = default)
{
return new (FromJson(type, stream));
}
public ValueTask<object> FromJsonAsync(Type type, DbDataReader reader, int index, CancellationToken cancellationToken = default)
{
return new (FromJson(type, reader, index));
}
public string ToCleanJson(object? document)
{
return ToJson(document);
}
public EnumStorage EnumStorage => EnumStorage.AsString;
public Casing Casing => Casing.Default;
public string ToJsonWithTypes(object document)
{
throw new NotSupportedException();
}
}
Next, replace the default ISerializer
when you bootstrap your DocumentStore
as in this example below:
var store = DocumentStore.For(_ =>
{
_.Connection("the connection string");
// Replace the ISerializer w/ the TestsSerializer
_.Serializer<TestsSerializer>();
});