Skip to content

JSON column support

Simon Hughes edited this page Mar 26, 2026 · 7 revisions
  • 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 👇

JSON Column to POCO Class Mapping

Overview

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.

Supported Databases

  • SQL Server: json type (SQL Server 2016+ and Azure SQL)
  • PostgreSQL: json and jsonb types

Quick Start

1. Define Your POCO Classes

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; }
    }
}

2. Configure the Mapping

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
    });
};

3. Generate Code

Run the generator as usual. The generated Order class will now have:

public Address ShippingAddress { get; set; }

instead of:

public string ShippingAddress { get; set; }

Configuration Options

JsonColumnMapping Properties

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.

Wildcard Matching

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"
});

Common Patterns

Pattern 1: Simple POCO

jsonColumnMappings.Add(new JsonColumnMapping
{
    Schema = "dbo",
    Table = "Users",
    Column = "Profile",
    PropertyType = "UserProfile",
    AdditionalNamespace = "MyApp.Models"
});

Pattern 2: Generic Collections

jsonColumnMappings.Add(new JsonColumnMapping
{
    Schema = "dbo",
    Table = "Products",
    Column = "Tags",
    PropertyType = "List<string>",
    AdditionalNamespace = "System.Collections.Generic"
});

Pattern 3: Dictionaries

jsonColumnMappings.Add(new JsonColumnMapping
{
    Schema = "dbo",
    Table = "Settings",
    Column = "Configuration",
    PropertyType = "Dictionary<string, object>",
    AdditionalNamespace = "System.Collections.Generic"
});

Pattern 4: Complex Nested Types

jsonColumnMappings.Add(new JsonColumnMapping
{
    Schema = "dbo",
    Table = "Reports",
    Column = "Data",
    PropertyType = "Dictionary<string, List<ReportItem>>",
    AdditionalNamespace = "System.Collections.Generic;MyApp.Models"
});

Pattern 5: Fully Qualified Type Names

jsonColumnMappings.Add(new JsonColumnMapping
{
    Schema = "dbo",
    Table = "Documents",
    Column = "Content",
    PropertyType = "MyCompany.Domain.Models.DocumentContent"
    // AdditionalNamespace is optional when using fully qualified names
});

Pattern 6: Configuring EF behaviour in a partial class (EF Core 8+)

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.

Nullable Reference Types and JSON Column Mappings

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.

The Two Settings

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.

How They Affect JSON-Mapped Column Properties

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; }

Why NullableReverseNavigationProperties Does Not Affect JSON Columns

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 in JSON-Mapped Columns

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.

Entity Framework Configuration

EF Core (prior to EF Core 8)

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));
}

EF Core with Owned Types (Recommended for EF Core 8+)

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();
});

Important Notes

  1. Type Compatibility: Ensure your POCO classes can be serialized/deserialized to/from JSON and match the structure in your database.

  2. 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.

  3. Partial class configuration: If you configure a JSON column yourself using OwnsOne/OwnsMany + ToJson() in a partial class, always set ExcludePropertyConfiguration = true. Without it, EF Core will throw a runtime error because the same property has been configured twice.

  4. Nullable Columns and Reference Types: Whether a nullable database column mapped to a class type generates MyClass? depends on Settings.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 as MyClass?, and #nullable enable is added to the file.
    • Settings.NullableReverseNavigationProperties has no effect on JSON-mapped column types.
    • Value types (e.g. int?, bool?) always follow standard nullable wrapping regardless of these settings.
  5. Default Values: Default values are cleared for JSON columns mapped to custom types, as they're typically not meaningful for complex types.

  6. 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 = true and handle it yourself in a partial class).

  7. Performance: Be aware that deserializing JSON on every query may have performance implications. Consider using projections or lazy loading patterns when appropriate.

Implementation Details

How It Works

  1. During code generation, the generator reads database schema including column types
  2. For each column with SQL type json or jsonb, it checks for a matching JsonColumnMapping
  3. If a match is found:
    • The column's PropertyType is changed from string to the specified type
    • Additional namespaces are added to the using statements for that table's files only
    • If ExcludePropertyConfiguration = true, the builder.Property(...) configuration line is omitted
    • Default values are cleared
  4. The generated code includes the strongly-typed property

Examples

See JsonColumnMappingExamples.cs for comprehensive examples of various usage scenarios.

Troubleshooting

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 a builder.Property(...) call for it
  • Set ExcludePropertyConfiguration = true on the JsonColumnMapping for 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 json or jsonb type 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 — AdditionalNamespace is 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 when AllowNullStrings is enabled.
  • Setting Settings.NullableReverseNavigationProperties = true will not add ? to JSON-mapped properties — it only affects one-to-one reverse navigation properties.

Clone this wiki locally