1

I am trying to use RestSharp to deserialize a JSON return into C# classes. The return sometimes contains a single, empty object when the data definition calls for an array of objects. If there is nothing to return the element should just be left out entirely, but that's not what is happening and I have no control over the return data structure.

Here is an example:

The person object should be an array. When there is actually data, it looks like this:

"person": [
  {
    "firstName": "John",
    "lastName": "Smith",
    "middleName": "B",
    "birthdate": "2021-06-09",
  },
  {
    "firstName": "test",
    "lastName": "Me",
    "birthdate": "2007-11-15",
  }
]

However, if there are no person objects to send at all, I am getting this:

"person": { }

This causes RestSharp to throw an error because it is expecting an array and got a single object.

Is there a way to deserialize this return and simply ignore these empty objects?

I tried using JsonSerializerSettings and setting NullValueHandling to Ignore and MissingMemberHandling to Ignore, but I still get an error. There are a lot of JsonSerializerSettings attributes, I suspect that one of them may be what I want but this isn't something that I do a lot.

3
  • 1
    Possible relate: How to handle both a single item and an array for the same property using System.Text.Json?. And you need to register the custom serializer to RestSharp: restsharp.dev/docs/advanced/serialization/#json Commented Aug 13 at 6:13
  • 3
    Post your service code, especially the classes. RestSharp doesn't have its own JSON serializer, it uses System.Text.Json or JSON.NET. The service shouldn't be returning an object instead of an array to begin with. That's a service bug. If there are no persons then person should be null, missing entirely or an empty array. If you own the service, fix it. If not, you'll have to customize deserialization eg using a custom type converter that checks the contents of the "person" attribute before actually deserializing it. Commented Aug 13 at 7:26
  • What happens if there's only a single person? Do you get "person":[{....}] or "person":{...} ? Does the service have an Open API schema that specifies what "person" should contain? Perhaps the service author decides to use a single object for 0-1 values by design. A bad design, but they probably won't change it, so you'll have to create a custom converter Commented Aug 13 at 7:31

1 Answer 1

1

Building on the answer from already linked SO post, we could write custom converter for the property with small tweaks.

First, here is example class that i have used:

public class TestDto
{
    [JsonConverter(typeof(SingleOrArrayConverter<Person>))]
    public List<Person> person { get; set; }
}

public class Person
{
    public string firstName { get; set; }
}

As you can see I annotated the property with [JsonConverter(typeof(SingleOrArrayConverter<Person>))] attribute for our converter to kick in.

Then I have adjusted mentioned converter - i added handling of unexcpected object while reading and also corrected wrtie method to just output an array no matter what:

public class SingleOrArrayConverter<TItem> : SingleOrArrayConverter<List<TItem>, TItem>
{
    public SingleOrArrayConverter() : this(true) { }
    public SingleOrArrayConverter(bool canWrite) : base(canWrite) { }
}

public class SingleOrArrayConverterFactory : JsonConverterFactory
{
    public bool CanWrite { get; }

    public SingleOrArrayConverterFactory() : this(true) { }

    public SingleOrArrayConverterFactory(bool canWrite) => CanWrite = canWrite;

    public override bool CanConvert(Type typeToConvert)
    {
        var itemType = GetItemType(typeToConvert);
        if (itemType == null)
            return false;
        if (itemType != typeof(string) && typeof(IEnumerable).IsAssignableFrom(itemType))
            return false;
        if (typeToConvert.GetConstructor(Type.EmptyTypes) == null || typeToConvert.IsValueType)
            return false;
        return true;
    }

    public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
    {
        var itemType = GetItemType(typeToConvert);
        var converterType = typeof(SingleOrArrayConverter<,>).MakeGenericType(typeToConvert, itemType);
        return (JsonConverter)Activator.CreateInstance(converterType, new object[] { CanWrite });
    }

    static Type GetItemType(Type type)
    {
        // Quick reject for performance
        if (type.IsPrimitive || type.IsArray || type == typeof(string))
            return null;
        while (type != null)
        {
            if (type.IsGenericType)
            {
                var genType = type.GetGenericTypeDefinition();
                if (genType == typeof(List<>))
                    return type.GetGenericArguments()[0];
                // Add here other generic collection types as required, e.g. HashSet<> or ObservableCollection<> or etc.
            }
            type = type.BaseType;
        }
        return null;
    }
}

public class SingleOrArrayConverter<TCollection, TItem> : JsonConverter<TCollection> where TCollection : class, ICollection<TItem>, new()
{
    public SingleOrArrayConverter() : this(true) { }
    public SingleOrArrayConverter(bool canWrite) => CanWrite = canWrite;

    public bool CanWrite { get; }

    public override TCollection Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        switch (reader.TokenType)
        {
            case JsonTokenType.Null:
                return null;
            case JsonTokenType.StartObject:
                // Consume and ignore unexpected object, when array is expected
                _ = JsonSerializer.Deserialize<TItem>(ref reader, options);
                return null;
            case JsonTokenType.StartArray:
                var list = new TCollection();
                while (reader.Read())
                {
                    if (reader.TokenType == JsonTokenType.EndArray)
                        break;
                    list.Add(JsonSerializer.Deserialize<TItem>(ref reader, options));
                }
                return list;
            default:
                throw new InvalidOperationException();
        }
    }

    public override void Write(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options)
    {
        writer.WriteStartArray();
        foreach (var item in value)
            JsonSerializer.Serialize(writer, item, options);
        writer.WriteEndArray();
    }
}

At last, following code worked seamlessly:

var restClient = new RestClient(configureSerialization: s => s.UseSystemTextJson());
var result = await restClient.GetAsync<TestDto>(url);
Sign up to request clarification or add additional context in comments.

Comments

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.