1

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": "clienta@yahoo.com",
            "useType": "Primary"
        }
    }
}

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

{
    "Emails": {
        "ContactEmail": [{
                "address": "clientb1@gmail.com",
                "useType": "Primary"
            }, {
                "address": "clientb2@gmail.com",
                "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?

Trevor Daniel
  • 3,785
  • 12
  • 53
  • 89
  • I would consider this poor design. Looks like you're going to need to write custom code for whatever serializer you're using. To help with that we'd need to know which. Newtonsoft? System.Text.Json? Utf8Json? They're all different. More info please. – McAden May 04 '21 at 14:03
  • I am using System.Text.Json - thanks for your message – Trevor Daniel May 04 '21 at 14:04
  • @McAden any help you could give with the "custom code" would be greatly appreciated. not sure where to start – Trevor Daniel May 04 '21 at 14:13

2 Answers2

0

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

McAden
  • 13,714
  • 5
  • 37
  • 63
  • I have updated my question as I have hit an additional problem with the JSON. Do you have any idea how to handle a complete empty object please? – Trevor Daniel May 05 '21 at 10:49
  • 1
    Wow! Whomever did this really dropped the ball. I'm really not sure if it's the cleanest way to resolve this but another converter would do the trick. Honestly at this point I'd raise an issue with their support as mutating responses and empty-strings instead of nulls is a pretty large amount of code-smell. – McAden May 05 '21 at 14:32
  • completely agree! – Trevor Daniel May 05 '21 at 15:10
0

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);
Andy
  • 12,859
  • 5
  • 41
  • 56