diff --git a/src/JsonApiDotNetCore/Serialization/JsonConverters/ResourceObjectConverter.cs b/src/JsonApiDotNetCore/Serialization/JsonConverters/ResourceObjectConverter.cs index 25218b1ba9..d0d37495a6 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonConverters/ResourceObjectConverter.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonConverters/ResourceObjectConverter.cs @@ -1,9 +1,11 @@ +using System.ComponentModel.DataAnnotations; using System.Reflection; using System.Text.Json; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Resources.Internal; using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCore.Serialization.Request; @@ -161,12 +163,18 @@ public override ResourceObject Read(ref Utf8JsonReader reader, Type typeToConver { var attributes = new Dictionary(); + // Should consider caching these per ResourceType. + HashSet nonNullableValueTypeAttributesRequired = resourceType.Attributes.Where(attr => + attr.Property.GetCustomAttribute() != null && attr.Property.PropertyType.IsValueType && + !RuntimeTypeConverter.CanContainNull(attr.Property.PropertyType)).ToHashSet(); + while (reader.Read()) { switch (reader.TokenType) { case JsonTokenType.EndObject: { + AddSentinelsForMissingRequiredValueTypeAttributes(attributes, nonNullableValueTypeAttributesRequired); return attributes; } case JsonTokenType.PropertyName: @@ -219,6 +227,19 @@ public override ResourceObject Read(ref Utf8JsonReader reader, Type typeToConver throw GetEndOfStreamError(); } + private static void AddSentinelsForMissingRequiredValueTypeAttributes(Dictionary incomingAttributes, + IEnumerable nonNullableValueTypeAttributesRequired) + { + foreach (AttrAttribute requiredAttribute in nonNullableValueTypeAttributesRequired) + { + if (!incomingAttributes.ContainsKey(requiredAttribute.PublicName)) + { + var attributeValue = new JsonMissingRequiredAttributeInfo(requiredAttribute.PublicName, requiredAttribute.Type.PublicName); + incomingAttributes.Add(requiredAttribute.PublicName, attributeValue); + } + } + } + /// /// Ensures that attribute values are not wrapped in s. /// diff --git a/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceObjectAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceObjectAdapter.cs index 1b85b35336..09e6f94be0 100644 --- a/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceObjectAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceObjectAdapter.cs @@ -63,12 +63,16 @@ private void ConvertAttribute(IIdentifiable resource, string attributeName, obje AssertIsKnownAttribute(attr, attributeName, resourceType, state); AssertNoInvalidAttribute(attributeValue, state); + AssertNoMissingRequiredAttribute(attributeValue, state); AssertSetAttributeInCreateResourceNotBlocked(attr, resourceType, state); AssertSetAttributeInUpdateResourceNotBlocked(attr, resourceType, state); AssertNotReadOnly(attr, resourceType, state); - attr.SetValue(resource, attributeValue); - state.WritableTargetedFields!.Attributes.Add(attr); + if (attributeValue is not JsonMissingRequiredAttributeInfo) + { + attr.SetValue(resource, attributeValue); + state.WritableTargetedFields!.Attributes.Add(attr); + } } private static void AssertIsKnownAttribute([NotNull] AttrAttribute? attr, string attributeName, ResourceType resourceType, RequestAdapterState state) @@ -96,6 +100,18 @@ private static void AssertNoInvalidAttribute(object? attributeValue, RequestAdap } } + private void AssertNoMissingRequiredAttribute(object? attributeValue, RequestAdapterState state) + { + if (state.Request.WriteOperation == WriteOperationKind.CreateResource) + { + if (attributeValue is JsonMissingRequiredAttributeInfo info) + { + throw new ModelConversionException(state.Position, "Required attribute is missing.", + $"The required attribute '{info.AttributeName}' on resource type '{info.ResourceName}' is missing."); + } + } + } + private static void AssertSetAttributeInCreateResourceNotBlocked(AttrAttribute attr, ResourceType resourceType, RequestAdapterState state) { if (state.Request.WriteOperation == WriteOperationKind.CreateResource && !attr.Capabilities.HasFlag(AttrCapabilities.AllowCreate)) diff --git a/src/JsonApiDotNetCore/Serialization/Request/JsonMissingRequiredAttributeInfo.cs b/src/JsonApiDotNetCore/Serialization/Request/JsonMissingRequiredAttributeInfo.cs new file mode 100644 index 0000000000..fda444494e --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Request/JsonMissingRequiredAttributeInfo.cs @@ -0,0 +1,19 @@ +namespace JsonApiDotNetCore.Serialization.Request; + +/// +/// A sentinel value that is temporarily stored in the attributes dictionary to postpone producing an error. +/// +internal sealed class JsonMissingRequiredAttributeInfo +{ + public string AttributeName { get; } + public string ResourceName { get; } + + public JsonMissingRequiredAttributeInfo(string attributeName, string resourceName) + { + ArgumentGuard.NotNull(attributeName); + ArgumentGuard.NotNull(resourceName); + + AttributeName = attributeName; + ResourceName = resourceName; + } +}