Serializing Object with Multiple String Properties to JSON Array in C#

By FoxLearn 1/14/2025 7:18:13 AM   83
If you're working with a Property class containing multiple string properties, and you need to serialize it into a JSON array where each string property becomes its own object, you're in the right place.

Suppose you have a RootObject class containing a list of Property objects like the following:

public class RootObject
{
    public List<Property> Properties { get; set; }
}

public class Property
{
    public string MyFirstProp { get; set; }
    public string MySecondProp { get; set; }
}

When serialized using Json.NET, the output looks like this:

{
  "Properties": [
    {
      "MyFirstProp": "Hello",
      "MySecondProp": "World"
    }
  ]
}

However, you'd like the output to look like this:

{
  "Properties": [
    {
      "MyFirstProp": "Hello"
    },
    {
      "MySecondProp": "World"
    }
  ]
}

The goal is to serialize each property of Property as a separate object in the JSON array.

To achieve this, you'll need to create a custom JsonConverter that serializes Property instances as arrays of single-property objects, rather than as a single object.

public class ObjectAsObjectArrayConverter<TObject> : JsonConverter<TObject>
{
    public override void WriteJson(JsonWriter writer, TObject value, JsonSerializer serializer)
    {
        var contract = (serializer.ContractResolver.ResolveContract(value.GetType()) as JsonObjectContract) 
            ?? throw new ArgumentException("Wrong contract type");

        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(property.PropertyName);
            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.WriteEndObject();
        }
        writer.WriteEndArray();
    }

    protected virtual bool ShouldSerialize(JsonProperty property, object value) =>
        property.Readable && !property.Ignored && (property.ShouldSerialize == null || property.ShouldSerialize(value));

    public override TObject ReadJson(JsonReader reader, Type objectType, TObject existingValue, bool hasExistingValue, JsonSerializer serializer)
    {
        if (existingValue == null)
            existingValue = (TObject)serializer.ContractResolver.ResolveContract(objectType).DefaultCreator();

        switch (reader.MoveToContentAndAssert().TokenType)
        {
            case JsonToken.Null:
                return (TObject)(object)null;

            case JsonToken.StartArray:
                while (reader.ReadToContentAndAssert().TokenType != JsonToken.EndArray)
                {
                    if (reader.TokenType == JsonToken.StartObject)
                    {
                        serializer.Populate(reader, existingValue);
                    }
                    else
                    {
                        throw new JsonSerializationException("Unexpected token type " + reader.TokenType.ToString());
                    }
                }
                break;

            case JsonToken.StartObject:
                serializer.Populate(reader, existingValue);
                break;

            default:
                throw new JsonSerializationException("Unexpected token type " + reader.TokenType.ToString());
        }
        return existingValue;
    }
}

Extensions for Handling JSON Readers

Next, we add some extension methods to handle JSON reading efficiently:

public static partial class JsonExtensions
{
    public static JsonReader ReadToContentAndAssert(this JsonReader reader) =>
        reader.ReadAndAssert().MoveToContentAndAssert();

    public static JsonReader MoveToContentAndAssert(this JsonReader reader)
    {
        if (reader == null)
            throw new ArgumentNullException();
        if (reader.TokenType == JsonToken.None)
            reader.ReadAndAssert();
        while (reader.TokenType == JsonToken.Comment)
            reader.ReadAndAssert();
        return reader;
    }

    public static JsonReader ReadAndAssert(this JsonReader reader)
    {
        if (reader == null)
            throw new ArgumentNullException();
        if (!reader.Read())
            throw new JsonReaderException("Unexpected end of JSON stream.");
        return reader;
    }
}

Updating the RootObject

With the custom converter in place, you can now modify your RootObject class to handle a single Property rather than a list:

public class RootObject
{
    public Property Properties { get; set; }
}

Serializing the Object

Finally, serialize your RootObject using the custom converter by adding it to the JsonSerializerSettings.Converters collection:

var settings = new JsonSerializerSettings
{
    Converters = { new ObjectAsObjectArrayConverter<Property>() },
};

var json = JsonConvert.SerializeObject(myClass, Formatting.Indented, settings);
Console.WriteLine(json);

Output

When serialized with the above setup, the output will look like this:

{
  "Properties": [
    {
      "MyFirstProp": "Hello"
    },
    {
      "MySecondProp": "World"
    }
  ]
}

By using a custom JsonConverter, we can serialize complex objects with multiple properties into a JSON array of objects with one property each. This approach allows for flexible and customized JSON serialization, which can be crucial in certain scenarios such as API integrations or data transformations.