2

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?

1
  • 1
    What should happen if the class contains a reference to some non-primitive member, say a List<string> or some other nested MyClass? Commented Jun 11, 2021 at 16:42

1 Answer 1

4

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.

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks so much for this! I haven't yet tried, but looking through it seems like it will work for me. This was the main part I was looking for: property.AttributeProvider.GetAttributes(typeof(CustomTypeAttribute) Appreciate the quick response!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.