Reputation: 353
I have a number of classes that I need to serialize into a standard format like below:
Class Example:
public class MyClass
{
[JsonProperty("prop1")]
public string Prop1 { get; set; }
[JsonProperty("prop2")]
[CustomType("somevalue")]
public string Prop2 { get; set; }
//
//
[JsonProperty("propn")]
[CustomType("anothervalue")]
public string PropN { get; set; }
}
I need my JSON to look like this:
{
"AllProps": [
{
"Key": "prop1",
"Value": "value of prop1",
"Type": "0" //default to "0" if CustomType attribute is null
},
{
"Key": "prop2",
"Value": "value of prop2",
"Type": "somevalue"
},
{
"Key": "propn",
"Value": "value of propn",
"Type": "anothervalue"
}
]
}
How can I carry those "CustomType" attributes forward into my JObject/JTokens?
Upvotes: 2
Views: 2182
Reputation: 116595
You can create a custom JsonConverter
that serializes your MyClass
in the required format by making use of the metadata stored in Json.NET's own JsonObjectContract
to get a list of all serializable properties and their attributes.
First, define the following converter and attribute:
[System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
public class CustomTypeAttribute : System.Attribute
{
public CustomTypeAttribute(string type) => this.Type = type;
public string Type { get; set; }
}
public class ObjectAsAllPropsConverter<TBase> : JsonConverter
{
const string AllPropsName = "AllProps";
const string KeyName = "Key";
const string ValueName = "Value";
const string TypeName = "Type";
const string DefaultType = "0";
static IContractResolver DefaultResolver { get; } = JsonSerializer.CreateDefault().ContractResolver;
readonly IContractResolver resolver;
public ObjectAsAllPropsConverter() : this(DefaultResolver) { }
public ObjectAsAllPropsConverter(IContractResolver resolver) => this.resolver = resolver ?? throw new ArgumentNullException(nameof(resolver));
public override bool CanConvert(Type objectType)
{
if (objectType.IsPrimitive || objectType == typeof(string) || !typeof(TBase).IsAssignableFrom(objectType))
return false;
return resolver.ResolveContract(objectType) is JsonObjectContract;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract(value.GetType());
writer.WriteStartObject();
writer.WritePropertyName(AllPropsName);
writer.WriteStartArray();
foreach (var property in contract.Properties.Where(p => ShouldSerialize(p, value)))
{
var propertyValue = property.ValueProvider.GetValue(value);
if (propertyValue == null && (serializer.NullValueHandling == NullValueHandling.Ignore || property.NullValueHandling == NullValueHandling.Ignore))
continue;
writer.WriteStartObject();
writer.WritePropertyName(KeyName);
writer.WriteValue(property.PropertyName);
writer.WritePropertyName(ValueName);
if (propertyValue == null)
writer.WriteNull();
else if (property.Converter != null && property.Converter.CanWrite)
property.Converter.WriteJson(writer, propertyValue, serializer);
else
serializer.Serialize(writer, propertyValue);
writer.WritePropertyName(TypeName);
var type = property.AttributeProvider.GetAttributes(typeof(CustomTypeAttribute), true).Cast<CustomTypeAttribute>().SingleOrDefault()?.Type ?? DefaultType;
writer.WriteValue(type);
writer.WriteEndObject();
}
writer.WriteEndArray();
writer.WriteEndObject();
}
protected virtual bool ShouldSerialize(JsonProperty property, object value) =>
property.Readable && !property.Ignored && (property.ShouldSerialize == null || property.ShouldSerialize(value));
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) => throw new NotImplementedException();
}
Now you can serialize to JSON as follows:
var myClass = new MyClass
{
Prop1 = "value of prop1",
Prop2 = "value of prop2",
PropN = "value of propn",
};
var settings = new JsonSerializerSettings
{
Converters = { new ObjectAsAllPropsConverter<object>() },
};
var json = JsonConvert.SerializeObject(myClass, Formatting.Indented, settings);
Which results in:
{
"AllProps": [
{
"Key": "prop1",
"Value": "value of prop1",
"Type": "0"
},
{
"Key": "prop2",
"Value": "value of prop2",
"Type": "somevalue"
},
{
"Key": "propn",
"Value": "value of propn",
"Type": "anothervalue"
}
]
}
Or if you need to serialize to an intermediate JObject
for some reason, you may do:
var token = JObject.FromObject(myClass, JsonSerializer.CreateDefault(settings));
Notes:
I only implemented serialization as deserialization was not requested in the question.
The converter's CanConvert
method automatically checks to see whether the incoming object type will be serialized as a JSON object. If you want all JSON objects to be serialized in the AllProps
format, use ObjectAsAllPropsConverter<object>
. If you only want a certain .NET type to be serialized in this format, restrict the generic constraint to this type, e.g.
var settings = new JsonSerializerSettings
{
Converters = { new ObjectAsAllPropsConverter<MyClass>() },
};
Demo fiddle here.
Upvotes: 4