Bug description
Feature request
When a complex type is mapped to a JSON column via ComplexProperty().ToJson(), and the stored JSON is missing a property that was added in a later version of the CLR model, EF Core currently materializes that property as null.
This happens even when the property has a CLR default value assigned via a property initializer or constructor.
This makes JSON schema evolution difficult for document-like models stored in relational databases.
For example:
- Version A of the application stores a JSON document without property
NewProperty.
- Version B of the application adds
NewProperty to the CLR type and initializes it to a default value.
- EF Core loads a row written by Version A.
- Since the JSON payload does not contain
NewProperty, EF Core materializes it as null.
- The default value from the CLR model is not preserved.
This is surprising because the application model expresses a valid default value, but that value is lost during EF materialization.
Desired behavior
It would be useful if EF Core provided a way to control how missing JSON members are handled when materializing complex types mapped with ComplexProperty().ToJson().
For example, one of the following behaviors would solve this scenario:
- Preserve CLR property initializers / constructor defaults when a JSON member is absent.
- Allow per-property configuration of a default value for absent JSON members.
- Allow a model-level or JSON-mapping-level option for missing-member handling.
- Allow a materialization hook specifically for JSON complex types before nullability validation / change tracking.
The important distinction is between:
- JSON member is present with value
null
- JSON member is absent because the row was written by an older application version
For schema evolution, an absent member should ideally be treated differently from an explicit null.
Why this matters
This is common when using JSON columns as document storage with versioned application models.
A typical deployment scenario is:
- Version A stores documents with schema A.
- Version B adds a new non-nullable property with a default value.
- Existing rows are still physically stored as schema A.
- Loading those rows through the Version B model produces partially invalid domain objects.
This creates a read hazard: application code expects the property to have its default value, but it is unexpectedly null.
It may also create a save hazard depending on nullability configuration, required properties, nested complex types, or validation logic.
The current workaround is to manually normalize the object graph after materialization, for example through ChangeTracker.Tracked, an IMaterializationInterceptor, or application-level post-load logic. However, this is error-prone and must be repeated for every JSON document type.
Another workaround is to backfill all existing JSON rows whenever a property is added, but this is not always practical for large tables, multi-tenant systems, or distributed deployments.
Example
using Microsoft.EntityFrameworkCore;
public class AppDbContext : DbContext
{
public DbSet<Record> Records => Set<Record>();
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseNpgsql("Host=localhost;Database=efrepro;Username=postgres;Password=postgres");
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Record>(entity =>
{
entity.ToTable("records");
entity.HasKey(x => x.Id);
entity.ComplexProperty(x => x.Document, document =>
{
document.ToJson("document");
});
});
}
}
public class Record
{
public Guid Id { get; set; }
public MyDocument Document { get; set; } = new();
}
public class MyDocument
{
// Existing property in schema version A.
public string Name { get; set; } = "";
// Added in schema version B.
// Existing JSON rows written by version A do not contain this property.
public string NewProperty { get; set; } = "default-value";
}
Assume an existing row contains JSON written by the previous application version:
{
"Name": "Document written by version A"
}
When the row is loaded by the newer application version, the expected materialized object would be:
record.Document.NewProperty == "default-value";
But the actual value is:
record.Document.NewProperty == null;
The CLR initializer is not preserved for the absent JSON member.
Current workaround
The workaround is to normalize the JSON document after materialization:
public class MyDocument
{
public string Name { get; set; } = "";
public string? NewProperty { get; set; } = "default-value";
public void Normalize()
{
NewProperty ??= "default-value";
}
}
For example:
using Microsoft.EntityFrameworkCore.Diagnostics;
public sealed class DocumentMaterializationInterceptor : IMaterializationInterceptor
{
public object InitializedInstance(
MaterializationInterceptionData materializationData,
object entity)
{
if (entity is Record record)
{
record.Document?.Normalize();
}
return entity;
}
}
However, this requires every application to implement its own schema-evolution/defaulting layer outside of EF Core.
Related issue
This looks related to:
That issue focuses on absent nested complex collections materializing as null instead of empty collections.
This feature request is for the more general schema-evolution case where any newly-added JSON-mapped complex type property may be absent from older stored JSON documents, and EF Core does not preserve CLR defaults during materialization.
EF Core version
EF Core version: [please fill in, e.g. 10.0.x or 11.0 preview x]
Database provider
Provider: [please fill in, e.g. Npgsql.EntityFrameworkCore.PostgreSQL / Microsoft.EntityFrameworkCore.SqlServer]
Target framework
Target framework: [please fill in, e.g. .NET 10.0 / .NET 11.0]
Your code
Stack traces
Verbose output
EF Core version
10
Database provider
Microsoft.EntityFrameworkCore.SqlServer
Target framework
.NET 10
Operating system
No response
IDE
No response
Bug description
Feature request
When a complex type is mapped to a JSON column via
ComplexProperty().ToJson(), and the stored JSON is missing a property that was added in a later version of the CLR model, EF Core currently materializes that property asnull.This happens even when the property has a CLR default value assigned via a property initializer or constructor.
This makes JSON schema evolution difficult for document-like models stored in relational databases.
For example:
NewProperty.NewPropertyto the CLR type and initializes it to a default value.NewProperty, EF Core materializes it asnull.This is surprising because the application model expresses a valid default value, but that value is lost during EF materialization.
Desired behavior
It would be useful if EF Core provided a way to control how missing JSON members are handled when materializing complex types mapped with
ComplexProperty().ToJson().For example, one of the following behaviors would solve this scenario:
The important distinction is between:
nullFor schema evolution, an absent member should ideally be treated differently from an explicit
null.Why this matters
This is common when using JSON columns as document storage with versioned application models.
A typical deployment scenario is:
This creates a read hazard: application code expects the property to have its default value, but it is unexpectedly
null.It may also create a save hazard depending on nullability configuration, required properties, nested complex types, or validation logic.
The current workaround is to manually normalize the object graph after materialization, for example through
ChangeTracker.Tracked, anIMaterializationInterceptor, or application-level post-load logic. However, this is error-prone and must be repeated for every JSON document type.Another workaround is to backfill all existing JSON rows whenever a property is added, but this is not always practical for large tables, multi-tenant systems, or distributed deployments.
Example
Assume an existing row contains JSON written by the previous application version:
{ "Name": "Document written by version A" }When the row is loaded by the newer application version, the expected materialized object would be:
But the actual value is:
The CLR initializer is not preserved for the absent JSON member.
Current workaround
The workaround is to normalize the JSON document after materialization:
For example:
However, this requires every application to implement its own schema-evolution/defaulting layer outside of EF Core.
Related issue
This looks related to:
That issue focuses on absent nested complex collections materializing as
nullinstead of empty collections.This feature request is for the more general schema-evolution case where any newly-added JSON-mapped complex type property may be absent from older stored JSON documents, and EF Core does not preserve CLR defaults during materialization.
EF Core version
EF Core version: [please fill in, e.g. 10.0.x or 11.0 preview x]
Database provider
Provider: [please fill in, e.g. Npgsql.EntityFrameworkCore.PostgreSQL / Microsoft.EntityFrameworkCore.SqlServer]
Target framework
Target framework: [please fill in, e.g. .NET 10.0 / .NET 11.0]
Your code
Stack traces
Verbose output
EF Core version
10
Database provider
Microsoft.EntityFrameworkCore.SqlServer
Target framework
.NET 10
Operating system
No response
IDE
No response