Fix Array in JSON Returned

111 views Asked by At

I am calling contacts from the LightSpeed API.

When i call for the details of client "A" the JSON contains the following for his related email(s)

{
    "Emails": {
        "ContactEmail": {
            "address": "[email protected]",
            "useType": "Primary"
        }
    }
}

When I call for the details of client "B" the JSON contains the following for this related email(s)

{
    "Emails": {
        "ContactEmail": [{
                "address": "[email protected]",
                "useType": "Primary"
            }, {
                "address": "[email protected]",
                "useType": "Secondary"
            }
        ]
    }
}

If I am correct I believe that the first response should be an array even if there is only 1 "email" returned...? because the system does allow for customers to have more than 1 email in their record.

Here is the class I am trying to Deserialize into. It works perfectly for client "B" but fails for client "A"

public class GetCustomersResponse
{
    public Attributes attributes { get; set; }
    public List<Customer> Customer { get; set; }
}

public class Attributes
{
    public string count { get; set; }
}

public class Customer
{
    public string customerID { get; set; }
    public string firstName { get; set; }
    public string lastName { get; set; }
    public string title { get; set; }
    public string company { get; set; }
    public string companyRegistrationNumber { get; set; }
    public string vatNumber { get; set; }
    public DateTime createTime { get; set; }
    public DateTime timeStamp { get; set; }
    public string archived { get; set; }
    public string contactID { get; set; }
    public string creditAccountID { get; set; }
    public string customerTypeID { get; set; }
    public string discountID { get; set; }
    public string taxCategoryID { get; set; }
    public Contact Contact { get; set; }
}

public class Contact
{
    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 Addresses Addresses { get; set; }
    public Phones Phones { get; set; }
    public Emails Emails { get; set; }
    public string Websites { get; set; }
    public DateTime timeStamp { get; set; }
}

public class Addresses
{
    public Contactaddress ContactAddress { get; set; }
}

public class Contactaddress
{
    public string address1 { get; set; }
    public string address2 { get; set; }
    public string city { get; set; }
    public string state { get; set; }
    public string zip { get; set; }
    public string country { get; set; }
    public string countryCode { get; set; }
    public string stateCode { get; set; }
}

public class Phones
{
    public List<Contactphone> ContactPhone { get; set; }
}

public class Contactphone
{
    public string number { get; set; }
    public string useType { get; set; }
}

public class Emails
{
    public List<Contactemail> ContactEmail { get; set; }
}

public class Contactemail
{
    public string address { get; set; }
    public string useType { get; set; }
}

I can't see me getting LightSpeed to change their API so can anyone suggest how to get the client with 1 email address to work with my class?

Any help would be greatly appreciated.

UPDATE:

with the help given I have got very close to some working code.

this is what I have for the custom json convertor:

public class ContactEmailJsonConverter : JsonConverter<List<ContactEmail>>
{
    public override List<ContactEmail> Read(
        ref Utf8JsonReader reader,
        Type typeToConvert,
        JsonSerializerOptions options)
    {
        try
        {
            if (reader.TokenType == JsonTokenType.StartArray)
            {
                return (List<ContactEmail>)JsonSerializer
                    .Deserialize(ref reader, typeToConvert, options);
            }
            else if (reader.TokenType == JsonTokenType.StartObject)
            {
                var email = (ContactEmail)JsonSerializer
                    .Deserialize(ref reader, typeof(ContactEmail), options);
                return new List<ContactEmail>(capacity: 1) { email };
            }
            else
            {
                throw new InvalidOperationException($"got: {reader.TokenType}");
            }
        }
        catch(Exception ex)
        {
            return null;
        }
    }

    public override void Write(Utf8JsonWriter writer, List<ContactEmail> value, JsonSerializerOptions options)
    {
        if ((value is null) || (value.Count == 0))
        {
            JsonSerializer.Serialize(writer, (ContactEmail)null, options);
        }
        else if (value.Count == 1)
        {
            JsonSerializer.Serialize(writer, value[0], options);
        }
        else
        {
            JsonSerializer.Serialize(writer, value, options);
        }
    }
}

But, I have now found a contact which appears not to have an email at all.. And the JSON returned by LightSpeed looks like this:

 "Emails":""

and it's breaking the converter code I have written. I am not sure how to handle this completely empty object?

2

There are 2 answers

2
Andy On BEST ANSWER

What you are going to have to do is create a couple custom JsonConverter objects. Let's say your models look like this:

public class ContactEmail
{
    [JsonPropertyName("address")]
    public string Address { get; set; }

    [JsonPropertyName("useType")]
    public string UseType { get; set; }
}

public class Emails
{
    public List<ContactEmail> ContactEmail { get; set; }
}

public class Root
{
    public Emails Emails { get; set; }
}

First, you need to set up a custom converter to handle List<ContactEmail> and the weird instances where it could be an array or single object:

public sealed class ContactEmailListJsonConverter 
    : JsonConverter<List<ContactEmail>>
{
    public override List<ContactEmail> Read(
        ref Utf8JsonReader reader,
        Type typeToConvert,
        JsonSerializerOptions options)
    {
        if(reader.TokenType == JsonTokenType.StartArray)
        {
            return (List<ContactEmail>)JsonSerializer
                .Deserialize(ref reader, typeToConvert, options);
        }
        else if (reader.TokenType == JsonTokenType.StartObject)
        {
            var email = (ContactEmail)JsonSerializer
                .Deserialize(ref reader, typeof(ContactEmail), options);
            return new List<ContactEmail>(capacity: 1) { email };
        }
        else
        {
            throw new InvalidOperationException($"got: {reader.TokenType}");
        }
    }

    public override void Write(
        Utf8JsonWriter writer,
        List<ContactEmail> value,
        JsonSerializerOptions options)
    {
        if((value is null) || (value.Count == 0))
        {
            JsonSerializer.Serialize(writer, (ContactEmail)null, options);
        }
        else if(value.Count == 1)
        {
            JsonSerializer.Serialize(writer, value[0], options);
        }
        else
        {
            JsonSerializer.Serialize(writer, value, options);
        }
    }
}

Second, you need to set up a custom converter to handle Emails and the weird instances where it's not an actual Emails object:

public sealed class EmailsJsonConverter : JsonConverter<Emails>
{
    public override Emails Read(
        ref Utf8JsonReader reader,
        Type typeToConvert,
        JsonSerializerOptions options)
    {
        // MUST be an object!
        if(reader.TokenType == JsonTokenType.StartObject)
        {
            return (Emails)JsonSerializer
                    .Deserialize(ref reader, typeToConvert, options);
        }
        // if it's not an object (ex: string), then just return null
        return null;
    }

    public override void Write(
        Utf8JsonWriter writer,
        Emails value,
        JsonSerializerOptions options)
    {
        JsonSerializer.Serialize(writer, value, options);
    }
}

Then you'd add the converters to your models:

public class Emails
{
    [JsonConverter(typeof(ContactEmailListJsonConverter))]
    public List<ContactEmail> ContactEmail { get; set; }
}

public class Root
{
    [JsonConverter(typeof(EmailsJsonConverter))]
    public Emails Emails { get; set; }
}

And simply deserialize as normal:

var obj = JsonSerializer.Deserialize<Root>(jsonData);
3
McAden On

I believe what you need is a converter. This is a quick-and-dirty so I'm not 100% but I believe it should get you pretty close (and I didn't bother going the other direction, just implemented Read, I figure you get the gist from this).

To use this, you'd mark up your Emails class with the converter:

public class Emails
{
    [JsonConverter(typeof(ContactEmailJsonConverter))]
    public Contactemail[] ContactEmail { get; set; }
}

And then write the converter like so. What I did here was basically look for [ at the beginning and if it's not there, wrap it before I deserilize.

public class ContactEmailJsonConverter : JsonConverter<Contactemail[]>
{
    public override Contactemail[] Read(
        ref Utf8JsonReader reader,
        Type typeToConvert,
        JsonSerializerOptions options) =>
        {
            string content = reader.GetString();
            if(!content.Trim().StartsWith("["))
            {
                content = $"[{content}]";
            }
            return JsonSerializer.Deserialize<Contactemail[]>(content);
        };

    public override void Write(
        Utf8JsonWriter writer,
        Contactemail[] cotactEmails,
        JsonSerializerOptions options) =>
            throw new NotImplementedException();
}

You can probably make it more robust but this should get you started.

Note: I did update the type to Contactemail[] because I think List<T> is a bit more complicated on the converter side. The documentation linked below states to use the "Factory Pattern" and gives examples so if you want to stick to List<T> you can follow that instead.

More documentation: https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to?pivots=dotnet-5-0