Skip to content

Comments

Fix complex property JSON column not marked nullable in TPH hierarchy#37781

Draft
Copilot wants to merge 2 commits intorelease/10.0from
copilot/fix-json-property-nullability
Draft

Fix complex property JSON column not marked nullable in TPH hierarchy#37781
Copilot wants to merge 2 commits intorelease/10.0from
copilot/fix-json-property-nullability

Conversation

Copy link
Contributor

Copilot AI commented Feb 24, 2026

In a TPH hierarchy, a JSON column backed by a complex property on a derived type was incorrectly created as NOT NULL, causing constraint violations when inserting rows for sibling types that don't have that property.

This was a parity gap vs. owned entity JSON columns, which already handle this via a BaseType != null check.

Changes

  • RelationalModel.CreateContainerColumn: In the complex property branch, after computing nullability from the property/chain, additionally check if chain[0].DeclaringType (the outermost declaring entity type) has a BaseType. If so, force IsNullable = true — matching the existing owned entity logic.

  • RelationalModelTest: Added Complex_property_json_column_is_nullable_in_TPH_hierarchy unit test with a 3-type TPH hierarchy (abstract base + two derived types, one with a ComplexProperty(...).ToJson()) asserting the resulting JSON column is nullable.

Example

public abstract class Entity { public Guid Id { get; init; } }

public class EntityWithoutComplexJson : Entity { }

public class EntityWithComplexJson : Entity
{
    public required ComplexEntity ComplexEntity { get; init; }
}

// Previously, "ComplexEntity" column was created as NOT NULL,
// causing a DB constraint violation when saving EntityWithoutComplexJson.
modelBuilder.Entity<EntityWithComplexJson>()
    .ComplexProperty(e => e.ComplexEntity)
    .ToJson();
Original prompt

This section details on the original issue you should resolve

<issue_title>Complex property stored as json will be marked non-nullable even in TPH class hierarchy</issue_title>
<issue_description>### Bug description

When two subclasses are stored in the same table using TPH, all columns mapped from properties that are exclusive to one of the subclasses will be marked as nullable, becuase if the row is an instance of the 'other' subclass that column will not have a value. Except for complex properties stored as json. This leads to exceptions when storing data because the database expect a value where there is none. This issue does not exist when using owned entities instead of complex entities.

Workaround is to explicitly specifiy IsRequired(false) but that doesn't seem the intended solution.

Your code

using System.Diagnostics;
using Microsoft.EntityFrameworkCore;

const string connectionString = "Server=localhost;Database=complex-issues;Port=5432;User Id=postgres;Password=postgres;Include Error Detail=true";
var options = new DbContextOptionsBuilder<AppDbContext>()
  .UseNpgsql(connectionString)
  .Options;
var dbContext = new AppDbContext(options);
await dbContext.Database.EnsureDeletedAsync();
await dbContext.Database.EnsureCreatedAsync();

var entity = new EntityWithoutComplexJson();
dbContext.Entities.Add(entity);
await dbContext.SaveChangesAsync();

public abstract class Entity
{
  public Guid Id { get; init; }
}

public class EntityWithoutComplexJson : Entity
{
  
}

public class EntityWithComplexJson : Entity
{
  public required ComplexEntity ComplexEntity { get; init; }
}

public class ComplexEntity
{
  public required string Value { get; init; }
}

public class AppDbContext(DbContextOptions options) : DbContext(options)
{
  public DbSet<Entity> Entities { get; init; }
  
  protected override void OnModelCreating(ModelBuilder modelBuilder)
  {
    base.OnModelCreating(modelBuilder);
    modelBuilder.Entity<EntityWithoutComplexJson>();
    modelBuilder.Entity<EntityWithComplexJson>().ComplexProperty(entity => entity.ComplexEntity).ToJson();
  }

  protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
  {
    base.OnConfiguring(optionsBuilder);
    optionsBuilder.LogTo(message =>
    {
      Debug.WriteLine(message);
    });
  }
}

Stack traces

---> Npgsql.PostgresException (0x80004005): 23502: null value in column "ComplexEntity" of relation "Entities" violates not-null constraint

DETAIL: Failing row contains (019b373e-f53a-7786-ac07-94575699cf3c, EntityWithoutComplexJson, null).
   at Npgsql.Internal.NpgsqlConnector.ReadMessageLong(Boolean async, DataRowLoadingMode dataRowLoadingMode, Boolean readingNotifications, Boolean isReadingPrependedMessage)
   at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
   at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
   at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
   at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
   at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
   at Npgsql.NpgsqlCommand.ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
  Exception data:
    Severity: ERROR
    SqlState: 23502
    MessageText: null value in column "ComplexEntity" of relation "Entities" violates not-null constraint
    Detail: Failing row contains (019b373e-f53a-7786-ac07-94575699cf3c, EntityWithoutComplexJson, null).
    SchemaName: public
    TableName: Entities
    ColumnName: ComplexEntity
    File: execMain.c
    Line: 1947
    Routine: ExecConstraints
   --- End of inner exception stack trace ---

Verbose output


EF Core version

10.0.1

Database provider

Npgsql.EntityFrameworkCore.PostgreSQL

Target framework

.NET 10.0

Operating system

MacOS

IDE

Rider</issue_description>

Comments on the Issue (you are @copilot in this section)


🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix complex property nullability in TPH hierarchy Fix complex property JSON column not marked nullable in TPH hierarchy Feb 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants