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.

12
  • 1
    Looks like you might need a custom converter. The issue, of course, is that "Websites" can either have type string or type object, which you can't encode using a C# object model Commented Dec 8, 2020 at 11:34
  • Remove extra : before your convert Commented Dec 8, 2020 at 11:47
  • @canton7... thxs. As a comparison, I tried to use Newtonsoft.Json... worked without issues. My preference would be to use Microsoft's System.Text.Json but I don't have the time to learn about custom converters... faced with a need to get business functionality out. :-( Commented Dec 8, 2020 at 11:50
  • the problem is "WebsitesResponse" is an object but you are sending string "", wonder how this JSON created, clearly it cannot be empty string. either fix your model or json, writing converted would produce more complexity. Commented Dec 8, 2020 at 11:51
  • @MichaelMao... which extra : are you referring to? Commented Dec 8, 2020 at 11:52

1 Answer 1

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; }
Sign up to request clarification or add additional context in comments.

5 Comments

I got WebsitesResponse do not define Dump()
@MichaelMao Sorry, I left debug code in there :)
@DavidG... Genius! :-) And for bonus points, the third party API does this multiple times for different nested json objects. Is it possible to make it generic or at least list each of these in the custom converter so that I don't have to have separate custom converters for each occurance? You get my vote for best answer!
@MashedSpud Added a generic version for you
@DavidG... thanks again! :-) Interestingly, when I tried Newtonsoft.Json, it seemed to work on my sample in the post above, but then failed with multiple occurrences of the same issue in a larger json payload. I guess I may have to put the effort into learning custom converters after all... they seem to have paid off in this case. I owe you a beer! :-)

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.