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.
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; }
}
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; }
User contributions licensed under CC BY-SA 3.0