JSON.NET customizing the serialization to exclude a property name - serialization

I am using Json.NET and I have the following code.
public class Row
{
[JsonProperty]
public IDictionary<string, object> Cells { get; set; }
}
The rows and cells are dynamically generated and I had C# dynamic/Expando feature to create those Cells. When Json.NET serializes the dynamic instances, it produces the correct json structure I want. However for a large data structure it has an adverse effect on the performance. For example, JsonSerializer calls DynamicObject TryGetMember quite extensively during the serialization. Therefore I needed a static data structure so the serialization would be much quicker. The synamic Expando object would still create dynamic properties, but I wanted the Json.NET serialization to use the static structure (created based on the dynamic structure) so the serialization would be much quicker.
Cells dictionary get populated based on the dynamic structure, and by invoking the JsonConvert, it produces the serialized json structure as below.
string serializeObject = JsonConvert.SerializeObject(data, Formatting.Indented);
//json output:
{
"Data": [
{
"Cells": {
"column1": "20",
"column2": "20"
}
},
{
"Cells": {
"column1": "20",
"column2": "20"
}
}
]
}
However the UI Grid which I’m binding to, require the below json structure
"Data": [
{
"column1": "20",
"column2": "20"
},
{
"column1": "20",
"column2": "20"
}
]
Is there a way I could remove the “Cells” and produce the Json structure as above?
I looked at the JSON.NET help docos and I could not find a way to achieve this.
Also tried overriding the DynamicContractResolver’s CreateProperty method to see if I can change the serialization behaviour, but I was unable to do so
public class DynamicContractResolver : DefaultContractResolver
{
protected override JsonProperty CreateProperty(System.Reflection.MemberInfo member, MemberSerialization memberSerialization)
{
if (member.Name == "Cells")
{
//remove the name "Cells" from the serialized structure
}
return base.CreateProperty(member, memberSerialization);
}
}
Or this is not simply supported? Any suggestions or directions much appreciated.

Found a way to get around this by creating a custom converter. The below code produces the Json result I need.
public class GridJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return true; //TODO: more logic check the type before the conversion..
}
public override void WriteJson(JsonWriter writer,
object value, JsonSerializer serializer)
{
var rows = (List<Row>)value;
writer.WriteStartObject();
writer.WritePropertyName("data");
writer.WriteStartArray();
foreach (var row in rows)
{
writer.WriteStartObject();
var cells = row.Cells;
foreach (var cell in cells)
{
writer.WritePropertyName(cell.Key);
writer.WriteValue(cell.Value);
}
writer.WriteEndObject();
}
writer.Flush();
}
public override object ReadJson(JsonReader reader, Type objectType,
object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
//Usage example:
string serializeObject = JsonConvert.SerializeObject
(someData, Formatting.Indented, new GridJsonConverter());

Related

Model binding a property attribute not working

I have an ASP.NET Core Web API.
I an endpoint which accepts a model called Search. It has property called Query of type Expression. This Expression object has sub classes.
public class Search {
public Expression Query{get;set;}
}
Public class Expression {
}
public class AndExpression {
public IList<Expression> Expressions {get;set;}
}
public class MatchesExpression {
public string FieldId {get;set;}
public string Value {get;set;}
public string Operator {get;set;}
}
I post the following JSON to my endpoint (content-type of application/json)
{
"query":
{
"fieldId": "body",
"value": "cake",
"operator": "matches"
}
}
Firstly, the query parameter is just the base Expression - A polymorphic issue!
So... I thought bespoke Model Binder.
I can set up a model binder against the Search object, but you'll note that the AndExpression can contain other Expression objects, so instead I'd like to write a binder that can be bound to "Query" on the Search Model and to Expressions on the AndExpression Model etc etc
I attempted this:
public class Search
{
[ModelBinder(BinderType = typeof(ExpressionBinder))]
public Expression Query { get; set; }
}
public class ExpressionBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
throw new NotImplementedException();
}
}
public class ExpressionBinderProvider : IModelBinderProvider {
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.ModelType == typeof(Expression))
{
return new BinderTypeModelBinder(typeof(ExpressionBinder));
}
return null;
}
}
Ive wired this binder up in the configureServices method of my Startup Class.
I have a break point in the ExpressionBinder and it doesn't hit!
What am I doing wrong?
Also, can I use the [ModelBinder(BinderType = typeof(ExpressionBinder))] attribute against a list of Expressions?
So this is very explicit https://github.com/aspnet/Mvc/issues/4553
If a FromBody attribute is applied the ModelBinder attribute wont work!

Derived type's properties missing in JSON response from ASP.NET Core API

The JSON response from my ASP.NET Core 3.1 API controller is missing properties. This happens when a property uses a derived type; any properties defined in the derived type but not in the base/interface will not be serialized to JSON. It seems there is some lack of support for polymorphism in the response, as if serialization is based on a property's defined type instead of its runtime type. How can I change this behavior to ensure that all public properties are included in the JSON response?
Example:
My .NET Core Web API Controller returns this object that has a property with an interface type.
// controller returns this object
public class Result
{
public IResultProperty ResultProperty { get; set; } // property uses an interface type
}
public interface IResultProperty
{ }
Here is a derived type that defines a new public property named Value.
public class StringResultProperty : IResultProperty
{
public string Value { get; set; }
}
If I return the derived type from my controller like this:
return new MainResult {
ResultProperty = new StringResultProperty { Value = "Hi there!" }
};
then the actual response includes an empty object (the Value property is missing):
I want the response to be:
{
"ResultProperty": { "Value": "Hi there!" }
}
While the other answers are good and solves the problem, if all you want is the general behavior to be like pre netcore3, you can use the Microsoft.AspNetCore.Mvc.NewtonsoftJson NuGet package and in Startup.cs do:
services.AddControllers().AddNewtonsoftJson()
More info here. This way, you don't need to create any extra json-converters.
I ended up creating a custom JsonConverter (System.Text.Json.Serialization namespace) which forces JsonSerializer to serialize to the object's runtime type. See the Solution section below. It's lengthy but it works well and does not require me to sacrifice object oriented principles in my API's design. (If you need something quicker and can use Newtonsoft then check out the top voted answer instead.)
Some background: Microsoft has a System.Text.Json serialization guide with a section titled Serialize properties of derived classes with good information relevant to my question. In particular it explains why properties of derived types are not serialized:
This behavior is intended to help prevent accidental exposure of data
in a derived runtime-created type.
If that is not a concern for you then the behavior can be overridden in the call to JsonSerializer.Serialize by either explicitly specifying the derived type or by specifying object, for example:
// by specifying the derived type
jsonString = JsonSerializer.Serialize(objToSerialize, objToSerialize.GetType(), serializeOptions);
// or specifying 'object' works too
jsonString = JsonSerializer.Serialize<object>(objToSerialize, serializeOptions);
To accomplish this with ASP.NET Core you need to hook into the serialization process. I did this with a custom JsonConverter that calls JsonSerializer.Serialize one of the ways shown above. I also implemented support for deserialization which, while not explicitly asked for in the original question, is almost always needed anyway. (Oddly, supporting only serialization and not deserialization proved to be tricky anyway.)
Solution
I created a base class, DerivedTypeJsonConverter, which contains all of the serialization & deserialization logic. For each of your base types, you would create a corresponding converter class for it that derives from DerivedTypeJsonConverter. This is explained in the numbered directions below.
This solution follows the "type name handling" convention from Json.NET which introduces support for polymorphism to JSON. It works by including an additional $type property in the derived type's JSON (ex: "$type":"StringResultProperty") that tells the converter what the object's true type is. (One difference: in Json.NET, $type's value is a fully qualified type + assembly name, whereas my $type is a custom string which helps future-proof against namespace/assembly/class name changes.) API callers are expected to include $type properties in their JSON requests for derived types. The serialization logic solves my original problem by ensuring that all of the object's public properties are serialized, and for consistency the $type property is also serialized.
Directions:
1) Copy the DerivedTypeJsonConverter class below into your project.
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
public abstract class DerivedTypeJsonConverter<TBase> : JsonConverter<TBase>
{
protected abstract string TypeToName(Type type);
protected abstract Type NameToType(string typeName);
private const string TypePropertyName = "$type";
public override bool CanConvert(Type objectType)
{
return typeof(TBase) == objectType;
}
public override TBase Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// get the $type value by parsing the JSON string into a JsonDocument
JsonDocument jsonDocument = JsonDocument.ParseValue(ref reader);
jsonDocument.RootElement.TryGetProperty(TypePropertyName, out JsonElement typeNameElement);
string typeName = (typeNameElement.ValueKind == JsonValueKind.String) ? typeNameElement.GetString() : null;
if (string.IsNullOrWhiteSpace(typeName)) throw new InvalidOperationException($"Missing or invalid value for {TypePropertyName} (base type {typeof(TBase).FullName}).");
// get the JSON text that was read by the JsonDocument
string json;
using (var stream = new MemoryStream())
using (var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Encoder = options.Encoder })) {
jsonDocument.WriteTo(writer);
writer.Flush();
json = Encoding.UTF8.GetString(stream.ToArray());
}
// deserialize the JSON to the type specified by $type
try {
return (TBase)JsonSerializer.Deserialize(json, NameToType(typeName), options);
}
catch (Exception ex) {
throw new InvalidOperationException("Invalid JSON in request.", ex);
}
}
public override void Write(Utf8JsonWriter writer, TBase value, JsonSerializerOptions options)
{
// create an ExpandoObject from the value to serialize so we can dynamically add a $type property to it
ExpandoObject expando = ToExpandoObject(value);
expando.TryAdd(TypePropertyName, TypeToName(value.GetType()));
// serialize the expando
JsonSerializer.Serialize(writer, expando, options);
}
private static ExpandoObject ToExpandoObject(object obj)
{
var expando = new ExpandoObject();
if (obj != null) {
// copy all public properties
foreach (PropertyInfo property in obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.CanRead)) {
expando.TryAdd(property.Name, property.GetValue(obj));
}
}
return expando;
}
}
2) For each of your base types, create a class that derives from DerivedTypeJsonConverter. Implement the 2 abstract methods which are for mapping $type strings to actual types. Here is an example for my IResultProperty interface that you can follow.
public class ResultPropertyJsonConverter : DerivedTypeJsonConverter<IResultProperty>
{
protected override Type NameToType(string typeName)
{
return typeName switch
{
// map string values to types
nameof(StringResultProperty) => typeof(StringResultProperty)
// TODO: Create a case for each derived type
};
}
protected override string TypeToName(Type type)
{
// map types to string values
if (type == typeof(StringResultProperty)) return nameof(StringResultProperty);
// TODO: Create a condition for each derived type
}
}
3) Register the converters in Startup.cs.
services.AddControllers()
.AddJsonOptions(options => {
options.JsonSerializerOptions.Converters.Add(new ResultPropertyJsonConverter());
// TODO: Add each converter
});
4) In requests to the API, objects of derived types will need to include a $type property. Example JSON: { "Value":"Hi!", "$type":"StringResultProperty" }
Full gist here
The documentation shows how to serialize as the derived class when calling the serializer directly. The same technique can also be used in a custom converter that we then can tag our classes with.
First, create a custom converter
public class AsRuntimeTypeConverter<T> : JsonConverter<T>
{
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return JsonSerializer.Deserialize<T>(ref reader, options);
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value, value?.GetType() ?? typeof(object), options);
}
}
Then mark the relevant classes to be used with the new converter
[JsonConverter(typeof(AsRuntimeTypeConverter<MyBaseClass>))]
public class MyBaseClass
{
...
Alternately, the converter can be registered in startup.cs instead
services
.AddControllers(options =>
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.Converters.Add(new AsRuntimeTypeConverter<MyBaseClass>());
}));
I had a similar issue, where I was returning an enumerable of type TAnimal (but the object instances were of derived types such as Dog, Cat, etc.):
[HttpGet]
public IEnumerable<TAnimal> GetAnimals()
{
IEnumerable<TAnimal> list = GetListOfAnimals();
return list;
}
This only included properties defined in TAnimal.
However, in ASP .NET Core 3.1 at least, I found that I could just cast the object instances to object, and the JSON serializer then included all the properties from the derived classes:
[HttpGet]
public IEnumerable<object> GetAnimals()
{
IEnumerable<TAnimal> list = GetListOfAnimals();
return list.Select(a => (object)a);
}
(Note that the signature of the GetAnimals method must also changed, but that doesn't usually matter much in a web API context). If you need to provide type information for Swagger or whatever, you can annotate the method:
[HttpGet]
[Produces(MediaTypeNames.Application.Json, Type = typeof(TAnimal[]))]
public IEnumerable<object> GetAnimals()
{
...
}
Casting to object is a simple solution if you only have a 1-layer-deep object hierarchy to worry about.
This is the expected result. You're upcasting when you do that, so what will be serialized is the upcasted object, not the actual derived type. If you need stuff from the derived type, then that has to be the type of the property. You may want to use generics for this reason. In other words:
public class Result<TResultProperty>
where TResultProperty : IResultProperty
{
public TResultProperty ResultProperty { get; set; } // property uses an interface type
}
Then:
return new Result<StringResultProperty> {
ResultProperty = new StringResultProperty { Value = "Hi there!" }
};
I solved it by writing this extension:
public static class JsonSerializationExtensions
{
public static string ToJson<T>(this IEnumerable<T> enumerable, bool includeDerivedTypesProperties = true)
where T : class
{
var jsonOptions = new JsonSerializerOptions()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
if (includeDerivedTypeProperties)
{
var collection = enumerable.Select(e => e as object).ToList();
return JsonSerializer.Serialize<object>(collection, jsonOptions);
}
else
{
return JsonSerializer.Serialize(enumerable, jsonOptions);
}
}
}
I was also struggling with this in a .NET Core 3.1 API, where I wanted the result to include $type attribute.
As suggested, install the correct package and then 'AddNewtonsoftJson'.
I wanted the $type field to be added to show the derived type handling, to get that
services.AddControllers().AddNewtonsoftJson(options =>
{
options.SerializerSettings.TypeNameHandling = Newtonsoft.Json.TypeNameHandling.All;
});
Not knocking Newtonsoft, but I found an easier way to resolve this with the built handlers.
[OperationContract]
[WebInvoke(Method = "GET", UriTemplate = "/emps", BodyStyle = WebMessageBodyStyle.Wrapped, RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)]
List<emp> GetEmps();
//[DataContract(Namespace = "foo")] <<< comment/removed this line
public class emp
{
public string userId { get; set; }
public string firstName { get; set; }
}
public class dept
{
public string deptId{ get; set; }
public string deptName{ get; set; }
}
In my case dept objects where working fine, but emp ones were not - they came across as empty.

passing an array to a asp net core web api action method HttpGet

I am trying to send an array of integers to my action method the code looks like so:
[HttpGet]
public async Task<IActionResult> ServicesByCategoryIds([FromQuery] int[] ids)
{
var services = await _accountsUow.GetServiceProfilesByCategoryIdsAsync(ids);
return Ok(services);
}
I call the method like so: https://localhost:44343/api/accounts/servicesbycategoryids?ids=1&ids=2
but always get en empty array when I call this method even tho I pass the ids in the query string. I am using .net core 2.1.
everything I have googled suggests that this is in fact the way this is done. . .
is there something I am missing here?
Thank you!
Binding failed for Array parameter is a known issue under Asp.Net Core 2.1 which has been recorded Array or List in query string does not get parsed #7712.
For a tempory workaround, you could set the FromQuery Name Property like below:
[HttpGet()]
[Route("ServicesByCategoryIds")]
public async Task<IActionResult> ServicesByCategoryIds([FromQuery(Name = "ids")]int[] ids)
{
return Ok();
}
A slight variation on Plamen's answer.
Arrays seem to have an empty GenericTypeArguments so added GetElementType()
Renamed class to avoid clashing with the framework class ArrayModelBinder.
Added a check on the element type as it's required.
More options for surrounding the array with brackets.
public class CustomArrayModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (!bindingContext.ModelMetadata.IsEnumerableType)
{
bindingContext.Result = ModelBindingResult.Failed();
return Task.CompletedTask;
}
var value = bindingContext.ValueProvider
.GetValue(bindingContext.ModelName)
.ToString();
if (string.IsNullOrWhiteSpace(value))
{
bindingContext.Result = ModelBindingResult.Success(null);
return Task.CompletedTask;
}
var elementType = bindingContext.ModelType.GetElementType() ??
bindingContext.ModelType.GetTypeInfo().GenericTypeArguments.FirstOrDefault();
if (elementType == null)
{
bindingContext.Result = ModelBindingResult.Failed();
return Task.CompletedTask;
}
var converter = TypeDescriptor.GetConverter(elementType);
var values = value.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(x => converter.ConvertFromString(Clean(x)))
.ToArray();
var typedValues = Array.CreateInstance(elementType, values.Length);
values.CopyTo(typedValues, 0);
bindingContext.Model = typedValues;
bindingContext.Result = ModelBindingResult.Success(bindingContext.Model);
return Task.CompletedTask;
}
private static string Clean(string str)
{
return str.Trim('(', ')').Trim('[', ']').Trim();
}
}
Then use with an IEnumerable<T>, IList<T> or array T[]
[ModelBinder(BinderType = typeof(CustomArrayModelBinder))] IEnumerable<T> ids
... T[] ids
... IList<T> ids
The parameter could be in path or query with optional brackets.
[Route("resources/{ids}")]
resource/ids/1,2,3
resource/ids/(1,2,3)
resource/ids/[1,2,3]
[Route("resources")]
resource?ids=1,2,3
resource?ids=(1,2,3)
resource?ids=[1,2,3]
I create a new web api class, with only one action.
[Produces("application/json")]
[Route("api/accounts")]
public class AccountsController : Controller
{
[HttpGet]
[Route("servicesbycategoryids")]
public IActionResult ServicesByCategoryIds([FromQuery] int[] ids)
{
return Ok();
}
}
Then use the same url as yours:
http://localhost:2443/api/accounts/servicesbycategoryids?ids=1&ids=2
It is working.
You can implement custom model binder and the ids to be part of the URI, not in the query string.
Your endpoint could look like this:
/api/accounts/servicesbycategoryids/(1,2)
public class ArrayModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
// Our binder works only on enumerable types
if (!bindingContext.ModelMetadata.IsEnumerableType)
{
bindingContext.Result = ModelBindingResult.Failed();
return Task.CompletedTask;
}
// Get the inputted value through the value provider
var value = bindingContext.ValueProvider
.GetValue(bindingContext.ModelName).ToString();
// If that value is null or whitespace, we return null
if (string.IsNullOrWhiteSpace(value))
{
bindingContext.Result = ModelBindingResult.Success(null);
return Task.CompletedTask;
}
// The value isn't null or whitespace,
// and the type of the model is enumerable.
// Get the enumerable's type, and a converter
var elementType = bindingContext.ModelType.GetTypeInfo().GenericTypeArguments[0];
var converter = TypeDescriptor.GetConverter(elementType);
// Convert each item in the value list to the enumerable type
var values = value.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries)
.Select(x => converter.ConvertFromString(x.Trim()))
.ToArray();
// Create an array of that type, and set it as the Model value
var typedValues = Array.CreateInstance(elementType, values.Length);
values.CopyTo(typedValues, 0);
bindingContext.Model = typedValues;
// return a successful result, passing in the Model
bindingContext.Result = ModelBindingResult.Success(bindingContext.Model);
return Task.CompletedTask;
}
}
Then use it in your action:
[HttpGet("({ids})", Name="GetAuthorCollection")]
public IActionResult GetAuthorCollection(
[ModelBinder(BinderType = typeof(ArrayModelBinder))] IEnumerable<int> ids)
{
//enter code here
}
Learned this from a pluralsight course: Building RESTful API with ASP.NET Core
The answer is that simply decorating the array with the [FromQuery] attribute is all that's needed to make the binding work. Without that attribute it fails to bind. That's it, and #kennyzx's answer above is best, but I feel like the point needed to be as simply stated as this: [FromQuery] is all you need. I don't know why these other answers went the ModelBinder route, maybe that is needed for some scenarios, but in my case and I'm sure with many others, the key was to not forget to apply the [FromQuery] attribute.
public ActionResult GetFoo(int id, [FromQuery] Guid[] someIds) { ... }
Rather than [ab]using query string (consider 1000s of IDs), you can use [FromBody] instead, and pass list of IDs as a JSON array:
public IActionResult ServicesByCategoryIds([FromBody] int[] ids)
As long as OpenAPI/Swagger is concerned, a proper specification will be generated:
"parameters": [
{
"name": "ids",
"in": "body",
"required": true,
"schema": {
"type": "array",
"items": {
"type": "integer",
"format": "int32"
}
}
}
],

Unwrap only some properties with Jackson

Assuming I have this objects:
class Person {
String name;
Household getHousehold();
}
class Household {
Set<Address> getAddresses();
String householdId;
}
which would normally be serialized as follows
{
"name": "XXX",
"household": {
"addresses": [...]
}
}
Is there a way to configure Jackson with annotations / mix-ins to obtain this (ie. without using DTO) ?
{
"name": "XXX",
"addresses": [...],
"household": {
"householdId": 123
}
}
You can configure the unwrapping of a specific property by both using mixins and annotations:
1. Mixins
Assuming you define the following mixin:
public abstract class UnwrappedAddresses {
#JsonUnwrapped
public abstract Household getHouseHold();
}
And then add a custom module to your objectMapper which applies the mixin to the Person class as follows:
ObjectMapper objectMapper = new ObjectMapper();
objectMapper .registerModule(new SimpleModule() {
#Override
public void setupModule(SetupContext context) {
context.setMixInAnnotations(Person.class, UnwrappedAddresses.class);
}
});
This approach does not change the Household serialization as a single item, but just unwraps a household item when it's encapsulated in a Person object.
2. Annotations
Just add #JsonUnwrapped to your getHouseHold() method.
EDIT: After post changes.
What you want is basically to change the output of the json, which can be done by using the #JsonAnyGetter annotation(which can dynamically add new properties to your pojo).
Your expected result can be achieved by ignoring the household property and unwrapping it with the help of the #JsonAnyGetter.
#JsonIgnoreProperties("houseHold")
public static class Person {
String name;
Household houseHold;
#JsonAnyGetter
public Map<String,Object> properties(){
Map<String,Object> additionalProps=new HashMap<>();
additionalProps.put("addresses", new ArrayList<>(houseHold.getAddresses()));
Map<String,Object> houseHolProps=new HashMap<>();
houseHolProps.put("houseHoldId", houseHold.id);
additionalProps.put("houseHold", houseHolProps);
return additionalProps;
}
..getters&setters omitted
}
Which would after serialization return
{"name":"name",
"houseHold":{"houseHoldId":0},
"addresses":[
{"houseNo":2,"street":"abc"},
{"houseNo":1,"street":"str"}
]
}

How to serialize a "union-like" field in C# with Json.NET

I am attempting to generate a JSON file that will be used within the Dojo javascript framework and would like to return a position attribute to be used in a dojo.place() call. The position parameter can be either a number or a string.
Using the StructLayoutwould not seem to work as-is since the serializer would try to emit both the String and Integer types. I'm looking at creating a custom ContractResolver that overrides the CreatePrimitiveContract to return a custom JsonConverter class. However, looking a the API, it appears that the JsonConverter is created based on type, and not a specific object value.
How can I handle this case in C# using the Json.NET serializer?
Presumably the solution would involve two properties with custom setters to null out the other property when one is set in conjunction with some sort of custom Json.Net class to inspect the values of the properties and only serialize the non-null one.
** Hypothetical Example **
// C# struct (or class)
[StructLayout(LayoutKind.Explicit)]
struct DojoPosition {
[JsonProperty(PropertyName="position")]
[FieldOffset(0)]
public String StrPos;
[JsonProperty(PropertyName="position")]
[FieldOffset(0)]
public Int32 IntPos;
}
// Serialization output
DojoPosition pos;
pos.StrPos = "only";
var output = JsonConvert.SerializeObject(pos);
// Output is: { "position": "only" }
pos.IntPos = 3;
var output = JsonConvert.SerializeObject(pos);
// Output is: { "position": 3 }
I just had a similiar problem.
For simple manipulation of a contract look there: Overriding the serialization behaviour in Json.Net
For resolving a JsonPrimitiveContract override the CreateContract method.
Here is an example based on our solution:
public class JsonDotNetContractResolver : DefaultContractResolver
{
protected override JsonContract CreateContract(Type objectType)
{
if (typeof(DojoPosition).IsAssignableFrom(objectType))
{
return new JsonPrimitiveContract(objectType.GetGenericArguments()[1])
{
CreatedType = typeof(object), // Not sure this will work for you, or is necessary...
IsReference = false,
Converter = DojoPositionConverter,
};
}
return base.CreateContract(objectType);
}
private class DojoPositionConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var dp = (DojoPosition) value;
if(string.IsNullOrEmpty(dp.StrPos))
serializer.Serialize(writer,dp.IntPos);
else
serializer.Serialize(writer,dp.StrPos);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
//...
}
public override bool CanConvert(Type objectType)
{
//....
}
}
}
How to determine the type to deserialize from the reader is your homework ;)