dotnet/efcore · error · InvalidOperationException

The optional complex property '{type}.{property}' is mapped

Error message

The optional complex property '{type}.{property}' is mapped to columns by flattening the contained properties into its container's table; this mapping requires at least one required property - to allow distinguishing between 'null' and empty values - but the complex type contains only optional properties. Configure the property with a shadow discriminator by adding a call to 'HasDiscriminator()' on the complex property configuration, or map this complex property to a JSON column instead.

What it means

ValidatePropertyMapping (RelationalModelValidator.cs:274-280) throws for an OPTIONAL complex property that is NOT mapped to JSON and whose complex type contains ONLY nullable properties. When such a complex property is flattened into the owner's table, EF needs at least one required column to distinguish 'the whole complex is null' from 'the complex is present but empty/all-null'. With all-optional columns that disambiguation is impossible, so validation rejects it.

Source

Thrown at src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs:278

    /// <inheritdoc />
    protected override void ValidatePropertyMapping(
        IComplexProperty complexProperty,
        IDiagnosticsLogger<DbLoggerCategory.Model.Validation> logger)
    {
        base.ValidatePropertyMapping(complexProperty, logger);

        if (complexProperty.IsCollection && !complexProperty.ComplexType.IsMappedToJson())
        {
            throw new InvalidOperationException(
                RelationalStrings.ComplexCollectionNotMappedToJson(
                    complexProperty.DeclaringType.DisplayName(), complexProperty.Name));
        }

        if (!complexProperty.ComplexType.IsMappedToJson()
            && complexProperty.IsNullable
            && complexProperty.ComplexType.GetProperties().All(m => m.IsNullable))
        {
            throw new InvalidOperationException(
                RelationalStrings.ComplexPropertyOptionalTableSharing(complexProperty.ComplexType.DisplayName(), complexProperty.Name));
        }

        if (complexProperty.GetJsonPropertyName() != null)
        {
            if (complexProperty.ComplexType.FindAnnotation(RelationalAnnotationNames.ContainerColumnName)?.Value is string columnName)
            {
                throw new InvalidOperationException(
                    RelationalStrings.ComplexPropertyBothJsonColumnAndJsonPropertyName(
                        $"{complexProperty.DeclaringType.DisplayName()}.{complexProperty.Name}",
                        columnName,
                        complexProperty.GetJsonPropertyName()));
            }

            if (!complexProperty.DeclaringType.IsMappedToJson())
            {
                throw new InvalidOperationException(
                    RelationalStrings.ComplexPropertyJsonPropertyNameWithoutJsonMapping(

View on GitHub (pinned to dbf9771522)

Solutions

  1. Make at least one property of the complex type required (IsRequired) so it can act as the null sentinel.
  2. Add a shadow discriminator via HasDiscriminator() on the complex property configuration.
  3. Map the complex property to a JSON column (ToJson()) instead of flattening, which sidesteps the sentinel requirement.

Example fix

// before - optional complex, all members nullable
modelBuilder.Entity<Customer>().ComplexProperty(c => c.Address, a =>
{
    a.Property(p => p.Street).IsRequired(false);
    a.Property(p => p.City).IsRequired(false);
}); // Address is nullable, all members nullable -> throws

// after - one required sentinel member
modelBuilder.Entity<Customer>().ComplexProperty(c => c.Address, a =>
{
    a.Property(p => p.Street).IsRequired();
});
// OR add a shadow discriminator: a.HasDiscriminator();
// OR a.ToJson("AddressJson");
Defensive patterns

Strategy: validation

Validate before calling

foreach (var et in context.Model.GetEntityTypes())
{
    foreach (var cp in et.GetComplexProperties())
    {
        if (!cp.IsCollection
            && cp.IsNullable
            && !cp.ComplexType.IsMappedToJson()
            && cp.ComplexType.GetProperties().All(p => p.IsNullable))
        {
            // will throw - make a member required, add HasDiscriminator, or use ToJson().
        }
    }
}

Prevention

When it happens

Trigger: Configuring an optional (nullable) complex property whose every member property is nullable, with table-sharing (flattened) mapping rather than JSON. Thrown at model validation.

Common situations: Value-object/complex types where all fields are legitimately optional (e.g. an Address with all-optional lines); making a complex property nullable to allow 'no value' but giving EF no sentinel column.

Related errors


AI-assisted analysis of dotnet/efcore@dbf9771522 (2026-08-06). Data as JSON: /api/errors/73ee309ffce01dad. Report an issue: GitHub.