Skip to content

Commit 18407ce

Browse files
committed
Produce error during serialization when required non-nullable value type attribute is missing in request body
1 parent 78d753c commit 18407ce

File tree

3 files changed

+58
-2
lines changed

3 files changed

+58
-2
lines changed

src/JsonApiDotNetCore/Serialization/JsonConverters/ResourceObjectConverter.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
using System.ComponentModel.DataAnnotations;
12
using System.Reflection;
23
using System.Text.Json;
34
using JetBrains.Annotations;
45
using JsonApiDotNetCore.Configuration;
56
using JsonApiDotNetCore.Resources;
67
using JsonApiDotNetCore.Resources.Annotations;
8+
using JsonApiDotNetCore.Resources.Internal;
79
using JsonApiDotNetCore.Serialization.Objects;
810
using JsonApiDotNetCore.Serialization.Request;
911

@@ -161,12 +163,18 @@ public override ResourceObject Read(ref Utf8JsonReader reader, Type typeToConver
161163
{
162164
var attributes = new Dictionary<string, object?>();
163165

166+
// Should consider caching these per ResourceType.
167+
HashSet<AttrAttribute> nonNullableValueTypeAttributesRequired = resourceType.Attributes.Where(attr =>
168+
attr.Property.GetCustomAttribute<RequiredAttribute>() != null && attr.Property.PropertyType.IsValueType &&
169+
!RuntimeTypeConverter.CanContainNull(attr.Property.PropertyType)).ToHashSet();
170+
164171
while (reader.Read())
165172
{
166173
switch (reader.TokenType)
167174
{
168175
case JsonTokenType.EndObject:
169176
{
177+
AddSentinelsForMissingRequiredValueTypeAttributes(attributes, nonNullableValueTypeAttributesRequired);
170178
return attributes;
171179
}
172180
case JsonTokenType.PropertyName:
@@ -219,6 +227,19 @@ public override ResourceObject Read(ref Utf8JsonReader reader, Type typeToConver
219227
throw GetEndOfStreamError();
220228
}
221229

230+
private static void AddSentinelsForMissingRequiredValueTypeAttributes(Dictionary<string, object?> incomingAttributes,
231+
IEnumerable<AttrAttribute> nonNullableValueTypeAttributesRequired)
232+
{
233+
foreach (AttrAttribute requiredAttribute in nonNullableValueTypeAttributesRequired)
234+
{
235+
if (!incomingAttributes.ContainsKey(requiredAttribute.PublicName))
236+
{
237+
var attributeValue = new JsonMissingRequiredAttributeInfo(requiredAttribute.PublicName, requiredAttribute.Type.PublicName);
238+
incomingAttributes.Add(requiredAttribute.PublicName, attributeValue);
239+
}
240+
}
241+
}
242+
222243
/// <summary>
223244
/// Ensures that attribute values are not wrapped in <see cref="JsonElement" />s.
224245
/// </summary>

src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceObjectAdapter.cs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,16 @@ private void ConvertAttribute(IIdentifiable resource, string attributeName, obje
6363

6464
AssertIsKnownAttribute(attr, attributeName, resourceType, state);
6565
AssertNoInvalidAttribute(attributeValue, state);
66+
AssertNoMissingRequiredAttribute(attributeValue, state);
6667
AssertSetAttributeInCreateResourceNotBlocked(attr, resourceType, state);
6768
AssertSetAttributeInUpdateResourceNotBlocked(attr, resourceType, state);
6869
AssertNotReadOnly(attr, resourceType, state);
6970

70-
attr.SetValue(resource, attributeValue);
71-
state.WritableTargetedFields!.Attributes.Add(attr);
71+
if (attributeValue is not JsonMissingRequiredAttributeInfo)
72+
{
73+
attr.SetValue(resource, attributeValue);
74+
state.WritableTargetedFields!.Attributes.Add(attr);
75+
}
7276
}
7377

7478
private static void AssertIsKnownAttribute([NotNull] AttrAttribute? attr, string attributeName, ResourceType resourceType, RequestAdapterState state)
@@ -96,6 +100,18 @@ private static void AssertNoInvalidAttribute(object? attributeValue, RequestAdap
96100
}
97101
}
98102

103+
private void AssertNoMissingRequiredAttribute(object? attributeValue, RequestAdapterState state)
104+
{
105+
if (state.Request.WriteOperation == WriteOperationKind.CreateResource)
106+
{
107+
if (attributeValue is JsonMissingRequiredAttributeInfo info)
108+
{
109+
throw new ModelConversionException(state.Position, "Required attribute is missing.",
110+
$"The required attribute '{info.AttributeName}' on resource type '{info.ResourceName}' is missing.");
111+
}
112+
}
113+
}
114+
99115
private static void AssertSetAttributeInCreateResourceNotBlocked(AttrAttribute attr, ResourceType resourceType, RequestAdapterState state)
100116
{
101117
if (state.Request.WriteOperation == WriteOperationKind.CreateResource && !attr.Capabilities.HasFlag(AttrCapabilities.AllowCreate))
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
namespace JsonApiDotNetCore.Serialization.Request;
2+
3+
/// <summary>
4+
/// A sentinel value that is temporarily stored in the attributes dictionary to postpone producing an error.
5+
/// </summary>
6+
internal sealed class JsonMissingRequiredAttributeInfo
7+
{
8+
public string AttributeName { get; }
9+
public string ResourceName { get; }
10+
11+
public JsonMissingRequiredAttributeInfo(string attributeName, string resourceName)
12+
{
13+
ArgumentGuard.NotNull(attributeName);
14+
ArgumentGuard.NotNull(resourceName);
15+
16+
AttributeName = attributeName;
17+
ResourceName = resourceName;
18+
}
19+
}

0 commit comments

Comments
 (0)