I would like to create and endpoint conforming to "JSON Merge Patch" https://www.rfc-editor.org/rfc/rfc7396
Please do not confuse it with "JavaScript Object Notation (JSON) Patch" https://www.rfc-editor.org/rfc/rfc6902
However, I have a slight problem with differentiating between two situations in request:
removal of property value, here email value is being removed:
{
surname: "Kowalski"
email: null
}
property not included because client simply does not want to update it, here email is not included because it should not be updated:
{
surname: "Kowalski"
}
The problem arises because in both situations after model binding email would have value null.
Do you have suggestion how this might be implemented?
You need 3 different states for email value here:
[email protected]
)null
value if email should be removedSo the problem actually is how to express these 3 states in string
property of your model. You can't do this with just raw string
property because null
value and missing value will conflict as you correctly described.
Solution is to use some flag that indicates whether the value was provided in the request. You could either have this flag as another property in your model or create a simple wrapper over string
, very similar to Nullable<T>
class.
I suggest creation of simple generic OptionalValue<T>
class:
public class OptionalValue<T>
{
private T value;
public T Value
{
get => value;
set
{
HasValue = true;
this.value = value;
}
}
public bool HasValue { get; set; }
}
Then you need custom JsonConverter
that could deserialize usual json value to OptionalValue<T>
:
class OptionalValueConverter<T> : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(OptionalValue<T>);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
return new OptionalValue<T>
{
Value = (T) reader.Value,
};
}
public override bool CanWrite => false;
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Your model will look something like this:
public class SomeModel
{
public string Surname { get; set; }
[JsonConverter(typeof(OptionalValueConverter<string>))]
public OptionalValue<string> Email { get; set; } = new OptionalValue<string>();
}
Note that you assign Email with empty OptionalValue<string>()
. If input json does not contains email
value than Email
property will keep it OptionalValue
with HasValue
set to false
.
If input json contains some email
, even null
, then OptionalValueConverter
will create instance of OptionalValue
with HasValue
set to true
.
Now in controller action you could determine any of 3 states for email
:
[HttpPatch]
public void Patch([FromBody]SomeModel data)
{
if (data.Email.HasValue)
{
// Email presents in Json
if (data.Email.Value == null)
{
// Email should be removed
}
else
{
// Email should be updated
}
}
else
{
// Email does not present in Json and should not be affected
}
}
Could you use the JsonMergePatch library? https://github.com/Morcatko/Morcatko.AspNetCore.JsonMergePatch
The usage is very simple:
[HttpPatch]
[Consumes(JsonMergePatchDocument.ContentType)]
public void Patch([FromBody] JsonMergePatchDocument<Model> patch)
{
...
patch.ApplyTo(backendModel);
...
}
It appears to support setting some properties to null, and leaving other properties untouched. Internally, the JsonMergePatchDocument creates a JsonPatch document, with one OperationType.Replace for each item in the request. https://github.com/Morcatko/Morcatko.AspNetCore.JsonMergePatch/blob/master/src/Morcatko.AspNetCore.JsonMergePatch/Formatters/JsonMergePatchInputFormatter.cs
This is a particular problem when using a language that doesn't support a distinction between undefined
and null
like JavaScript and TypeScript do. There are other options which you might consider:
""
to delete it because an empty string is often not a valid value (also not always feasible)X-MYAPP-SET-EMAIL=true
will delete email if it is null). Downside is that this could blow up your request and pain for client developersEach option from above has its own drawbacks so think carefully before you decide which way you go.
I come to this thread with the same question. My solution is similar to the 'CodeFuller' one but more complete as it covers API documentation with swagger and better because uses less code. It also uses System.text.json
instead of the Newtonsoft
library.
Define your model by taking advantaging of the existent Optional
struct (no need to create a new OptionalValue
class)
{
public string Surname { get; set; }
[JsonConverter(typeof(OptionalConverter<string>))]
public Optional<string> Email { get; set; } = default;
}
Tell Swagger (if applicable) to format as a string input/type for a better client experience:
c.MapType<Optional<string>>(() => new OpenApiSchema { Type = "string" });
Add a custom JSON converter based on System.text.json
:
public class OptionalConverter<T> : JsonConverter<Optional<T>>
{
// https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to
public override bool CanConvert(Type typeToConvert) =>
typeToConvert == typeof(Optional<T>);
public override Optional<T> Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options) =>
new Optional<T>(JsonSerializer.Deserialize<T>(ref reader, options));
public override void Write(
Utf8JsonWriter writer,
Optional<T> value,
JsonSerializerOptions options) =>
throw new NotImplementedException("OptionalValue is not suppose to be written");
}
That is. Now you have 3 states:
[HttpPatch]
[Consumes("application/merge-patch+json")]
public void Patch([FromBody]SomeModel data)
{
if (data.Email.HasValue)
{
// Email presents in Json
if (data.Email.Value == null)
{
// Email should be removed
}
else
{
// Email should be updated
}
}
else
{
// Email does not present in Json and should not be affected
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With