-
Notifications
You must be signed in to change notification settings - Fork 11
Advanced Tips
ℹ️ This is actually a Newtonsoft.Json feature, not a Nevermore feature - but it's worth considering.
When a document contains a property or collection, and the instances within them are different to what the property or collection is declared as, Newtonsoft.Json (the default serializer in Nevermore) will add a $type
property. For example, perhaps we have something like this:
class OrderHistory
{
public string Id { get; set; }
public List<IAuditEvent> AuditEvents { get; } = new List<IAuditEvent>();
}
interface IAuditEvent { }
class CreatedEvent : IAuditEvent { ... }
class EditedEvent : IAuditEvent { ... }
The serialized JSON will end up something like this:
{
"AuditEvents": [
{
"$type": "Company.Product.Namespace.CreatedEvent, Company.Product"
// ...
},
{
"$type": "Company.Product.Namespace.EditedEvent, Company.Product"
// ...
},
{
"$type": "Company.Product.Namespace.EditedEvent, Company.Product",
// ...
}
]
}
Not only does this look messy, but it also makes data migration difficult over time. If you rename a class you won't be able to deserialize it.
A feature you should make use of in Json.Net is a custom ISerializationBinder
. This allows you to control the format used in the $type
property.
Some things this would let you do:
- Serialize just the class name (
EditedEvent
for example), and on deserialization, look them up by searching for types with that name in the assemblies we'd expect. - Handle situations where a type has moved, by translating one name to another.
- Use attributes or something else on types to declare names and discover/map them.
One thing to keep in mind though is that the serialization binder is called each time you deserialize, so you may want to cache the mappings.
Here's an example serialization binder:
class MySerializationBinder : ISerializationBinder
{
readonly ISerializationBinder fallback;
public MySerializationBinder(ISerializationBinder fallback)
{
this.fallback = fallback ?? new DefaultSerializationBinder();
}
public Type BindToType(string? assemblyName, string typeName)
{
if (typeName == "Created" && assemblyName == null)
return typeof(CreatedEvent);
if (typeName == "Edited" && assemblyName == null)
return typeof(EditedEvent);
return fallback.BindToType(assemblyName, typeName);
}
public void BindToName(Type serializedType, out string? assembly, out string? typeName)
{
if (serializedType == typeof(CreatedEvent))
{
typeName = "Created";
assemblyName = null;
return;
}
if (serializedType == typeof(EditedEvent))
{
typeName = "Edited";
assemblyName = null;
return;
}
fallback.BindToName(serializedType, out assemblyName, out typeName);
}
}
It's registered like this:
configuration.UseJsonNetSerialization(settings =>
{
settings.SerializationBinder = new MySerializationBinder(settings.SerializationBinder);
});
From SQL 2016, the IsJson
function can be used to check that a document is valid JSON (though not that it has all the documents we expect). You might consider adding this as a constraint:
create table Customer
(
Id nvarchar(200) primary key not null,
-- ... other properties...
[JSON] nvarchar(max) not null
constraint CK_Customer_JSON check (IsJson([JSON]) > 0)
)
If you use Compression you can check those too:
[JSONBlob] varbinary(max) not null
constraint CK_Customer_JSONBlob
check (IsJson(cast(DECOMPRESS([JSONBlob]) as nvarchar(max))) > 0)
The benefit to this is that it will prevent someone adding improperly-formatted JSON, or JSON that the application understands (e.g., UTF-8 encoded) but SQL doesn't.
⚠️ Note that there is a performance penalty to this - for JSON stored as text, it's about 20-50% overhead (like 0.2ms for a small document, and 2ms for a large 65K document). For compressed JSON, it's closer to 100%. But this only affects writes, not reads, so it may be worth it.
Alternatively, if the application isn't busy, you could use ISJSON to do an offline check on a schedule to pick up errors.
Overview
Getting started
Extensibility
Misc