C# System.Text.Json The JSON value could not be converted to Class structure

1

I am consuming a third party API which I cannot change that sometimes returns a standard JSON structure and at other times, it does not. I suspect the reason is that the data in the 3rd party application is optional within the UI. If the data were present, it would look like this:

{
    "contactID": "1234",
    :
    <some other attributes>
    :
    "Websites": {
        "ContactWebsite": {
            "url": "www.foo.com"
        }
    }
}

When it isn't present, it looks like this...

{
    "id": "1234",
    :
    <some other attributes>
    :
    "Websites": ""
}

My code looks like...

using System;
using System.Text.Json;

namespace TestJsonError
{
    class Program
    {
        static void Main(string[] args)
        {
            string content = "{ \"contactID\": \"217\", \"custom\": \"\", \"noEmail\": \"true\", \"noPhone\": \"true\", \"noMail\": \"true\", \"Websites\": \"\", \"timeStamp\": \"2020-10-13T19:21:38+00:00\" }";

            Console.WriteLine($"{content}");

            var response = JsonSerializer.Deserialize<ContactResponse>(content);

            Console.WriteLine($"contactID={response.contactID}");
        }
    }

    public class ContactResponse
    {
        public string contactID { get; set; }
        public string custom { get; set; }
        public string noEmail { get; set; }
        public string noPhone { get; set; }
        public string noMail { get; set; }
        public WebsitesResponse Websites { get; set; }
        public DateTime timeStamp { get; set; }
    }

    public class WebsitesResponse
    {
        public ContactWebsiteResponse ContactWebsite { get; set; }
    }

    public class ContactWebsiteResponse
    {
        public string url { get; set; }
    }
}

I get the following error...

> System.Text.Json.JsonException   HResult=0x80131500   Message=The JSON
> value could not be converted to TestJsonError.WebsitesResponse. Path:
> $.Websites | LineNumber: 0 | BytePositionInLine: 106.  
> Source=System.Text.Json   StackTrace:    at
> System.Text.Json.ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(Type
> propertyType)    at
> System.Text.Json.JsonPropertyInfoNotNullable`4.OnRead(ReadStack&
> state, Utf8JsonReader& reader)    at
> System.Text.Json.JsonPropertyInfo.Read(JsonTokenType tokenType,
> ReadStack& state, Utf8JsonReader& reader)    at
> System.Text.Json.JsonSerializer.ReadCore(JsonSerializerOptions
> options, Utf8JsonReader& reader, ReadStack& readStack)    at
> System.Text.Json.JsonSerializer.ReadCore(Type returnType,
> JsonSerializerOptions options, Utf8JsonReader& reader)    at
> System.Text.Json.JsonSerializer.Deserialize(String json, Type
> returnType, JsonSerializerOptions options)    at
> System.Text.Json.JsonSerializer.Deserialize[TValue](String json,
> JsonSerializerOptions options)    at
> TestJsonError.Program.Main(String[] args) in
> C:\Users\simon\source\repos\TestJsonError\TestJsonError\Program.cs:line
> 14
> 
>   This exception was originally thrown at this call stack:
>     [External Code]
>     TestJsonError.Program.Main(string[]) in Program.cs

Any suggestions?


An edit...

I'm kind of embarrassed to suggest this alternative approach to DavidG's elegant response below, but for the sake of completeness I thought I'd share. Be warned... it's dirty but paradoxically kind of elegant.

It seems that the API I am calling, when it doesn't have data to return, it sends back an empty string (""). I have observed in other APIs when this situation arises the API returns null.

If I replace "Websites": "" with "Websites": null and then JsonSerializer.Deserialize, it works a treat. Should I use this approach? Well, I know it's dirty, but because i have to pepper Json classes with so many lines to cater for this situation, actually the code seems easier to understand. I spect there will be a performance overhead, but I'm not dealing with high volumes so it will probably be OK in my case.

Happy to take alternative opinions.

c#
json
.net-core
deserialization
asked on Stack Overflow Dec 8, 2020 by Mashed Spud • edited Dec 8, 2020 by Mashed Spud

1 Answer

3

You can do this with a custom converter. For example:

public class WebsitesConverter : JsonConverter<WebsitesResponse>
{
    public override WebsitesResponse Read(ref Utf8JsonReader reader, Type typeToConvert, 
        JsonSerializerOptions options)
    {
        if(reader.TokenType == JsonTokenType.String)
        {
            // You can either return this, or a null object if you prefer
            return new WebsitesResponse
            {
                ContactWebsite = new ContactWebsiteResponse
                {
                    url = reader.GetString()
                }
            };
        }
        
        return JsonSerializer.Deserialize<WebsitesResponse>(ref reader);            
    }

    public override void Write(Utf8JsonWriter writer, WebsitesResponse value, 
        JsonSerializerOptions options)
    {
        throw new NotImplementedException();
    }
}

And change your model to register the converter:

public class ContactResponse
{
    public string contactID { get; set; }
    public string custom { get; set; }
    public string noEmail { get; set; }
    public string noPhone { get; set; }
    public string noMail { get; set; }
    [JsonConverter(typeof(WebsitesConverter))]
    public WebsitesResponse Websites { get; set; }
    public DateTime timeStamp { get; set; }
}

Bonus!

If you wanted to make it a little more generic, this would work:

public class StringObjectConverter<T> : JsonConverter<T> where T : class
{
    public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if(reader.TokenType == JsonTokenType.String)
        {
            return null;
        }
        
        return JsonSerializer.Deserialize<T>(ref reader);
        
    }

    public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
    {
        throw new NotImplementedException();
    }
}

And your model would change to:

[JsonConverter(typeof(StringObjectConverter<WebsitesResponse>))]
public WebsitesResponse Websites { get; set; }
answered on Stack Overflow Dec 8, 2020 by DavidG • edited Dec 8, 2020 by DavidG

User contributions licensed under CC BY-SA 3.0