JSON Columns in EF Core: Storing Complex Objects as JSON

Not every piece of data fits neatly into separate relational columns. Configuration blobs, metadata, nested structures — sometimes JSON is the right storage format. EF Core 7 introduced support for mapping .NET objects to JSON columns, letting you store complex structures in a single column while still querying into them with LINQ.

Basic JSON Column Mapping

Consider a product with shipping dimensions that vary wildly between product types:

Example.cs
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }
    public ProductMetadata Metadata { get; set; } = new();
}

public class ProductMetadata
{
    public double WeightKg { get; set; }
    public Dimensions? Dimensions { get; set; }
    public List<string> Tags { get; set; } = [];
    public Dictionary<string, string> Attributes { get; set; } = new();
}

public class Dimensions
{
    public double LengthCm { get; set; }
    public double WidthCm { get; set; }
    public double HeightCm { get; set; }
}

Map the metadata to a JSON column using ToJson():

Example.cs
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Product>()
        .OwnsOne(p => p.Metadata, metadata =>
        {
            metadata.ToJson();
            metadata.OwnsOne(m => m.Dimensions);
        });
}

The Products table now has a Metadata column containing JSON like:

data.json
{
  "WeightKg": 2.5,
  "Dimensions": { "LengthCm": 30, "WidthCm": 20, "HeightCm": 10 },
  "Tags": ["fragile", "electronics"],
  "Attributes": { "Colour": "Black", "Material": "Aluminium" }
}

Querying into JSON Columns

The real power is LINQ support. EF Core translates property access on JSON-mapped objects into SQL JSON path queries:

Example.cs
// Filter by a property inside the JSON
var heavyProducts = await context.Products
    .Where(p => p.Metadata.WeightKg > 10)
    .ToListAsync();

// Filter by nested property
var tallProducts = await context.Products
    .Where(p => p.Metadata.Dimensions != null
        && p.Metadata.Dimensions.HeightCm > 50)
    .ToListAsync();

// Order by a JSON property
var products = await context.Products
    .OrderByDescending(p => p.Metadata.WeightKg)
    .ToListAsync();

On SQL Server, these translate to JSON_VALUE expressions:

query.sql
SELECT * FROM [Products]
WHERE CAST(JSON_VALUE([Metadata], '$.WeightKg') AS float) > 10

Updating JSON Data

Updates work naturally through the object model:

Example.cs
var product = await context.Products.FindAsync(1);

product!.Metadata.WeightKg = 3.0;
product.Metadata.Tags.Add("updated");
product.Metadata.Dimensions = new Dimensions
{
    LengthCm = 35,
    WidthCm = 25,
    HeightCm = 12
};

await context.SaveChangesAsync();

EF Core detects changes within the JSON structure and updates the entire JSON column. It doesn't do partial JSON updates — the whole column is rewritten.

Collections in JSON

JSON columns can contain collections, including collections of complex objects:

Example.cs
public class Order
{
    public int Id { get; set; }
    public string CustomerName { get; set; } = string.Empty;
    public List<OrderEvent> EventLog { get; set; } = [];
}

public class OrderEvent
{
    public string EventType { get; set; } = string.Empty;
    public DateTime OccurredAt { get; set; }
    public string? Details { get; set; }
}
Example.cs
modelBuilder.Entity<Order>()
    .OwnsMany(o => o.EventLog, events =>
    {
        events.ToJson();
    });

Query collections with standard LINQ:

Example.cs
var recentlyShipped = await context.Orders
    .Where(o => o.EventLog.Any(e =>
        e.EventType == "Shipped"
        && e.OccurredAt > DateTime.UtcNow.AddDays(-7)))
    .ToListAsync();

Projecting JSON Properties

Select specific properties from JSON columns in projections:

Example.cs
var summaries = await context.Products
    .Select(p => new
    {
        p.Name,
        Weight = p.Metadata.WeightKg,
        TagCount = p.Metadata.Tags.Count
    })
    .ToListAsync();

JSON Columns vs Separate Tables

When should you use JSON columns instead of a related table?

Use JSON when:

Use separate tables when:

Provider Support

JSON column support varies by database provider:

Limitations

Example.cs
// For simple lists, use a value converter instead
modelBuilder.Entity<Product>()
    .Property(p => p.Tags)
    .HasConversion(
        v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default),
        v => JsonSerializer.Deserialize<List<string>>(v, JsonSerializerOptions.Default)!);

JSON columns strike a pragmatic balance between relational rigour and the flexibility that real-world applications need. Use them for genuinely semi-structured data, and keep your core domain in proper relational columns.