-
Notifications
You must be signed in to change notification settings - Fork 224
JSON column support
- For adding
[JsonPropertyName("your_name")]attribute or other custom attributes, please look at Extended Property Names Feature - Support for JSON columns is now supported.
- By default, they map to a string, or
- you can specify what C# class they map to, see below 👇
This feature allows you to map JSON database columns directly to C# POCO classes instead of the default string type. This eliminates the need for manual deserialization in your application code and provides strong typing for your JSON data.
-
SQL Server:
jsontype (SQL Server 2016+ and Azure SQL) -
PostgreSQL:
jsonandjsonbtypes
First, create the POCO classes that match your JSON structure:
namespace MyApp.Models
{
public class Address
{
public string Street { get; set; }
public string City { get; set; }
public string PostalCode { get; set; }
public string Country { get; set; }
}
}In your settings (typically in your .tt file or Settings.cs), configure the JSON column mappings:
Settings.AddJsonColumnMappings = delegate (List<JsonColumnMapping> jsonColumnMappings)
{
jsonColumnMappings.Add(new JsonColumnMapping
{
Schema = "dbo",
Table = "Orders",
Column = "ShippingAddress",
PropertyType = "Address",
AdditionalNamespace = "MyApp.Models" // Optional
});
};Run the generator as usual. The generated Order class will now have:
public Address ShippingAddress { get; set; }instead of:
public string ShippingAddress { get; set; }| Property | Type | Default | Description |
|---|---|---|---|
Schema |
string |
Database schema name. Use "*" to match any schema. |
|
Table |
string |
Table name. Use "*" to match any table. |
|
Column |
string |
Column name to map. | |
PropertyType |
string |
C# type to use (e.g., "Address", "List<string>", "Dictionary<string, object>"). |
|
AdditionalNamespace |
string |
null |
Namespace(s) required for the type, scoped to this table's generated files only. Separate multiple namespaces with semicolons. |
ExcludePropertyConfiguration |
bool |
false |
When true, suppresses the generated builder.Property(...) fluent configuration for this column. Use this when you configure the column yourself in a partial class (e.g. using OwnsMany/ToJson), to avoid the "property can only be configured once" runtime error. |
You can use wildcards to apply mappings broadly:
// Map all "Metadata" columns across all tables and schemas
jsonColumnMappings.Add(new JsonColumnMapping
{
Schema = "*",
Table = "*",
Column = "Metadata",
PropertyType = "Dictionary<string, object>",
AdditionalNamespace = "System.Collections.Generic"
});jsonColumnMappings.Add(new JsonColumnMapping
{
Schema = "dbo",
Table = "Users",
Column = "Profile",
PropertyType = "UserProfile",
AdditionalNamespace = "MyApp.Models"
});jsonColumnMappings.Add(new JsonColumnMapping
{
Schema = "dbo",
Table = "Products",
Column = "Tags",
PropertyType = "List<string>",
AdditionalNamespace = "System.Collections.Generic"
});jsonColumnMappings.Add(new JsonColumnMapping
{
Schema = "dbo",
Table = "Settings",
Column = "Configuration",
PropertyType = "Dictionary<string, object>",
AdditionalNamespace = "System.Collections.Generic"
});jsonColumnMappings.Add(new JsonColumnMapping
{
Schema = "dbo",
Table = "Reports",
Column = "Data",
PropertyType = "Dictionary<string, List<ReportItem>>",
AdditionalNamespace = "System.Collections.Generic;MyApp.Models"
});jsonColumnMappings.Add(new JsonColumnMapping
{
Schema = "dbo",
Table = "Documents",
Column = "Content",
PropertyType = "MyCompany.Domain.Models.DocumentContent"
// AdditionalNamespace is optional when using fully qualified names
});When using EF Core 8+ owned-entity JSON features (OwnsOne/OwnsMany + ToJson()), you configure the column in a partial class rather than through the generated fluent API. In this case, set ExcludePropertyConfiguration = true to prevent the generator from also emitting builder.Property(...), which would cause a "property can only be configured once" runtime error.
// In your settings file:
jsonColumnMappings.Add(new JsonColumnMapping
{
Schema = "dbo",
Table = "SpatialElementPreset",
Column = "InteractionEffects",
PropertyType = "List<SpatialElementInteractionEffect>",
AdditionalNamespace = "MyApp.Models",
ExcludePropertyConfiguration = true // Suppress the generated builder.Property(...) call
});Then, in a partial class, configure it yourself:
namespace MyApp.Models
{
public partial class SpatialElementPresetConfiguration
{
partial void InitializePartial(EntityTypeBuilder<SpatialElementPreset> builder)
{
builder.OwnsMany(e => e.InteractionEffects, nav =>
{
nav.ToJson("InteractionEffects");
nav.OwnsOne(x => x.Args);
});
}
}
}The generator will still emit the strongly-typed property on the POCO class, but will leave the fluent configuration entirely to your partial class.
Two settings control whether the generator emits #nullable enable and nullable annotations (?). Understanding how they interact with JSON-mapped columns is important for getting the output you expect.
| Setting | Default | Purpose |
|---|---|---|
Settings.AllowNullStrings |
false |
Enables ? on nullable reference-type properties: string, byte[], and JSON-mapped class types. Also adds #nullable enable to generated files. |
Settings.NullableReverseNavigationProperties |
false |
Makes one-to-one reverse navigation properties nullable (e.g. public virtual Order? Order). Also adds #nullable enable to generated files. Has no effect on JSON-mapped column types. |
#nullable enable is added to a generated file whenever either setting is true.
The ? nullable annotation on a JSON-mapped class type is controlled only by AllowNullStrings. NullableReverseNavigationProperties does not make JSON-mapped types nullable — it only affects one-to-one reverse navigation properties.
Given a nullable database column (allows NULL) mapped to Address:
AllowNullStrings |
NullableReverseNavigationProperties |
#nullable enable emitted |
Generated property |
|---|---|---|---|
false (default) |
false (default) |
No | public Address ShippingAddress { get; set; } |
false |
true |
Yes | public Address ShippingAddress { get; set; } |
true |
false |
Yes | public Address? ShippingAddress { get; set; } |
true |
true |
Yes | public Address? ShippingAddress { get; set; } |
Given a non-nullable database column (NOT NULL) mapped to Address:
AllowNullStrings |
NullableReverseNavigationProperties |
#nullable enable emitted |
Generated property |
|---|---|---|---|
false (default) |
false (default) |
No | public Address ShippingAddress { get; set; } |
false |
true |
Yes | public Address ShippingAddress { get; set; } |
true |
false |
Yes | public Address ShippingAddress { get; set; } |
true |
true |
Yes | public Address ShippingAddress { get; set; } |
NullableReverseNavigationProperties is specifically for the case where a parent entity optionally has a related child entity in a one-to-one relationship — the parent can exist without the child, so the reverse navigation on the parent should be ChildEntity?.
JSON-mapped columns are regular data properties, not navigation properties. Their nullability is a data concern (does this column allow NULL?), which is why it is controlled by AllowNullStrings alongside all other reference-type property nullability.
Value types (e.g. int, bool, DateTime) used as JSON-mapped PropertyType values always follow standard nullable wrapping: if the database column allows NULL, the generated type will be int?, bool?, etc., regardless of AllowNullStrings. This is because Nullable<T> for value types is valid C# in all contexts and does not require #nullable enable.
In your DbContext.OnModelCreating method:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// For simple types
modelBuilder.Entity<Order>()
.Property(e => e.ShippingAddress)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
v => JsonSerializer.Deserialize<Address>(v, (JsonSerializerOptions)null));
// For nullable types
modelBuilder.Entity<Order>()
.Property(e => e.Metadata)
.HasConversion(
v => v == null ? null : JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
v => v == null ? null : JsonSerializer.Deserialize<Dictionary<string, object>>(v, (JsonSerializerOptions)null));
}Use ExcludePropertyConfiguration = true on the mapping (see Pattern 6 above) and configure the column in a partial class:
builder.OwnsOne(o => o.ShippingAddress, sa =>
{
sa.ToJson();
});-
Type Compatibility: Ensure your POCO classes can be serialized/deserialized to/from JSON and match the structure in your database.
-
Namespace Requirements: If your type requires additional namespaces, specify them in
AdditionalNamespace. The namespace is added only to the generated files for that specific table — it will not appear on every other generated model. -
Partial class configuration: If you configure a JSON column yourself using
OwnsOne/OwnsMany+ToJson()in a partial class, always setExcludePropertyConfiguration = true. Without it, EF Core will throw a runtime error because the same property has been configured twice. -
Nullable Columns and Reference Types: Whether a nullable database column mapped to a class type generates
MyClass?depends onSettings.AllowNullStrings:- When
AllowNullStrings = false(the default): JSON-mapped class types are always generated without?, even if the database column allows NULL. The column can still be null at runtime — the annotation is simply omitted. - When
AllowNullStrings = true: JSON-mapped class types that map to a nullable column are generated asMyClass?, and#nullable enableis added to the file. -
Settings.NullableReverseNavigationPropertieshas no effect on JSON-mapped column types. - Value types (e.g.
int?,bool?) always follow standard nullable wrapping regardless of these settings.
- When
-
Default Values: Default values are cleared for JSON columns mapped to custom types, as they're typically not meaningful for complex types.
-
Entity Framework Configuration: You must configure EF to handle serialization/deserialization. The generator creates the property with the correct type but doesn't add EF configuration automatically (unless you use
ExcludePropertyConfiguration = trueand handle it yourself in a partial class). -
Performance: Be aware that deserializing JSON on every query may have performance implications. Consider using projections or lazy loading patterns when appropriate.
- During code generation, the generator reads database schema including column types
- For each column with SQL type
jsonorjsonb, it checks for a matchingJsonColumnMapping - If a match is found:
- The column's
PropertyTypeis changed fromstringto the specified type - Additional namespaces are added to the using statements for that table's files only
- If
ExcludePropertyConfiguration = true, thebuilder.Property(...)configuration line is omitted - Default values are cleared
- The column's
- The generated code includes the strongly-typed property
See JsonColumnMappingExamples.cs for comprehensive examples of various usage scenarios.
Q: My custom type isn't recognized
- Ensure the namespace is correct in
AdditionalNamespace - Verify the type is accessible in your project
- Check that you're using the correct casing
Q: EF throws a "property can only be configured once" error
- You are configuring the column in a partial class (e.g. with
OwnsMany/ToJson) and the generator is also emitting abuilder.Property(...)call for it - Set
ExcludePropertyConfiguration = trueon theJsonColumnMappingfor that column
Q: EF throws serialization errors
- Verify you've configured EF to handle JSON serialization (see "Entity Framework Configuration" above)
- Ensure your POCO structure matches the JSON in the database
Q: The mapping isn't being applied
- Verify the schema, table, and column names match exactly (case-insensitive comparison is used)
- Check that the column is actually a
jsonorjsonbtype in the database - Ensure you're calling the generator after configuring the mappings
Q: The additional namespace is appearing on all my generated models
- This was a bug in earlier versions. Upgrade to the latest version —
AdditionalNamespaceis now scoped to the POCO and configuration files for the specific table only.
Q: My JSON-mapped class property doesn't have ? even though the column allows NULL
- Set
Settings.AllowNullStrings = true. JSON-mapped class types (reference types) only get the?nullable annotation whenAllowNullStringsis enabled. - Setting
Settings.NullableReverseNavigationProperties = truewill not add?to JSON-mapped properties — it only affects one-to-one reverse navigation properties.