Skip to content

Fail on missing required non-nullable value type in request body #1254

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;

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

// Should consider caching these per ResourceType.
HashSet<AttrAttribute> nonNullableValueTypeAttributesRequired = resourceType.Attributes.Where(attr =>
attr.Property.GetCustomAttribute<RequiredAttribute>() != 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:
Expand Down Expand Up @@ -219,6 +227,19 @@ public override ResourceObject Read(ref Utf8JsonReader reader, Type typeToConver
throw GetEndOfStreamError();
}

private static void AddSentinelsForMissingRequiredValueTypeAttributes(Dictionary<string, object?> incomingAttributes,
IEnumerable<AttrAttribute> 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);
}
}
}

/// <summary>
/// Ensures that attribute values are not wrapped in <see cref="JsonElement" />s.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace JsonApiDotNetCore.Serialization.Request;

/// <summary>
/// A sentinel value that is temporarily stored in the attributes dictionary to postpone producing an error.
/// </summary>
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;
}
}