Implementing "JSON Merge Patch" in ASP.NET Core - best way do differentiate between null and not defined property
You need 3 different states for email value here:
- Filled value for update (e.g.
[email protected]
) null
value if email should be removed- Missing value if email should not be touched.
So 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
}
}
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:
- use PUT (not always feasible)
- for strings use
""
to delete it because an empty string is often not a valid value (also not always feasible) - add an extra custom header to indicate if you really want to delete that value with a default value set to false (e.g.
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 developers
Each option from above has its own drawbacks so think carefully before you decide which way you go.
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