{"record":{"id":"73ee309ffce01dad","repo":"dotnet/efcore","slug":"the-optional-complex-property-type-property","errorCode":null,"errorMessage":"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.","messagePattern":"The optional complex 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\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs","lineNumber":278,"sourceCode":"    /// <inheritdoc />\n    protected override void ValidatePropertyMapping(\n        IComplexProperty complexProperty,\n        IDiagnosticsLogger<DbLoggerCategory.Model.Validation> logger)\n    {\n        base.ValidatePropertyMapping(complexProperty, logger);\n\n        if (complexProperty.IsCollection && !complexProperty.ComplexType.IsMappedToJson())\n        {\n            throw new InvalidOperationException(\n                RelationalStrings.ComplexCollectionNotMappedToJson(\n                    complexProperty.DeclaringType.DisplayName(), complexProperty.Name));\n        }\n\n        if (!complexProperty.ComplexType.IsMappedToJson()\n            && complexProperty.IsNullable\n            && complexProperty.ComplexType.GetProperties().All(m => m.IsNullable))\n        {\n            throw new InvalidOperationException(\n                RelationalStrings.ComplexPropertyOptionalTableSharing(complexProperty.ComplexType.DisplayName(), complexProperty.Name));\n        }\n\n        if (complexProperty.GetJsonPropertyName() != null)\n        {\n            if (complexProperty.ComplexType.FindAnnotation(RelationalAnnotationNames.ContainerColumnName)?.Value is string columnName)\n            {\n                throw new InvalidOperationException(\n                    RelationalStrings.ComplexPropertyBothJsonColumnAndJsonPropertyName(\n                        $\"{complexProperty.DeclaringType.DisplayName()}.{complexProperty.Name}\",\n                        columnName,\n                        complexProperty.GetJsonPropertyName()));\n            }\n\n            if (!complexProperty.DeclaringType.IsMappedToJson())\n            {\n                throw new InvalidOperationException(\n                    RelationalStrings.ComplexPropertyJsonPropertyNameWithoutJsonMapping(","sourceCodeStart":260,"sourceCodeEnd":296,"githubUrl":"https://github.com/dotnet/efcore/blob/dbf9771522148d61a2467854921bd5dc6f6e6916/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs#L260-L296","documentation":"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.","triggerScenarios":"Configuring an optional (nullable) complex property whose every member property is nullable, with table-sharing (flattened) mapping rather than JSON. Thrown at model validation.","commonSituations":"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.","solutions":["Make at least one property of the complex type required (IsRequired) so it can act as the null sentinel.","Add a shadow discriminator via HasDiscriminator() on the complex property configuration.","Map the complex property to a JSON column (ToJson()) instead of flattening, which sidesteps the sentinel requirement."],"exampleFix":"// before - optional complex, all members nullable\nmodelBuilder.Entity<Customer>().ComplexProperty(c => c.Address, a =>\n{\n    a.Property(p => p.Street).IsRequired(false);\n    a.Property(p => p.City).IsRequired(false);\n}); // Address is nullable, all members nullable -> throws\n\n// after - one required sentinel member\nmodelBuilder.Entity<Customer>().ComplexProperty(c => c.Address, a =>\n{\n    a.Property(p => p.Street).IsRequired();\n});\n// OR add a shadow discriminator: a.HasDiscriminator();\n// OR a.ToJson(\"AddressJson\");","handlingStrategy":"validation","validationCode":"foreach (var et in context.Model.GetEntityTypes())\n{\n    foreach (var cp in et.GetComplexProperties())\n    {\n        if (!cp.IsCollection\n            && cp.IsNullable\n            && !cp.ComplexType.IsMappedToJson()\n            && cp.ComplexType.GetProperties().All(p => p.IsNullable))\n        {\n            // will throw - make a member required, add HasDiscriminator, or use ToJson().\n        }\n    }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Ensure optional complex types flattened into a table have at least one required member.","Prefer HasDiscriminator() or ToJson() when all members are legitimately optional.","Review nullable complex types during model review."],"tags":["complex-type","nullable","table-sharing","json-mapping","model-validation"],"analyzedSha":"dbf9771522148d61a2467854921bd5dc6f6e6916","analyzedAt":"2026-08-06T20:46:03.226Z","schemaVersion":2},"datasetVersion":"2026-08-07T03:17:09.362Z"}