1

I'm working with an API that is returning results to me in a different way than I'm used to dealing with, and seemingly non-standard.

For example, here's a snippet of Customer data:

{
    "CustomerID": {
        "value": "EXAMPLE"
    },
    "CustomerCurrencyID": {
        "value": "USD"
    }
}

That "value" property seems very unnecessary, so I would like to see if I can just bypass that all together and deserialize that JSON into an object like so:

class Customer {
    public string CustomerID { get; set; }
    public string CustomerCurrencyID { get; set; }
}

I'm currently working on writing a custom JsonConverter to handle this, so if I'm heading down the right path just let me know, but any tips/tricks here would be much appreciated!

3
  • 2
    That, or create the C# object representing the format you want, write the C# code to map from bad object to good object, and serialize that. I would opt for that over a custom serializer, but that's mostly personal preference. Commented May 6, 2021 at 15:10
  • @Jonesopolis The thing is, I'm leaning more towards a custom converter because when I want to send my object BACK to the API, it would need to be in their format. Commented May 6, 2021 at 15:12
  • Makes sense. Or map both ways - an extension class with two methods static GoodApiObject ConvertFromBadAPIObject(this badApiObject) and static BadApiObject ConvertBackToBadApiObject(this goodApiObject). It can be good practice to wrap third party integrations in objects you own, depending on the scenario. Commented May 6, 2021 at 15:14

1 Answer 1

6

You can do this with a generic custom JsonConverter such as the following:

public class WrapWithValueConverter<TValue> : JsonConverter
{
    // Here we take advantage of the fact that a converter applied to a property has highest precedence to avoid an infinite recursion.
    class DTO { [JsonConverter(typeof(NoConverter))] public TValue value { get; set; } public object GetValue() => value; }

    public override bool CanConvert(Type objectType) => typeof(TValue).IsAssignableFrom(objectType);
    
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        => serializer.Serialize(writer, new DTO { value = (TValue)value });

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        => serializer.Deserialize<DTO>(reader)?.GetValue();
}

public class NoConverter : JsonConverter
{
    // NoConverter taken from this answer https://stackoverflow.com/a/39739105/3744182
    // By https://stackoverflow.com/users/3744182/dbc
    // To https://stackoverflow.com/questions/39738714/selectively-use-default-json-converter
    public override bool CanConvert(Type objectType)  { throw new NotImplementedException(); /* This converter should only be applied via attributes */ }
    public override bool CanRead => false;
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) => throw new NotImplementedException();
    public override bool CanWrite => false;
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => throw new NotImplementedException();
}

Then you can apply it to your model as follows:

class Customer {
    [JsonConverter(typeof(WrapWithValueConverter<string>))]
    public string CustomerID { get; set; }
    [JsonConverter(typeof(WrapWithValueConverter<string>))]
    public string CustomerCurrencyID { get; set; }
}

Demo fiddle #1 here.

Or, if you want all strings to be wrapped in a {"value": <string value>} object, you can add the converter to JsonSerializerSettings.Converters when serializing and deserializing:

var settings = new JsonSerializerSettings
{
    Converters = { new WrapWithValueConverter<string>() },
};

var model = JsonConvert.DeserializeObject<Customer>(json, settings);

var json2 = JsonConvert.SerializeObject(model, Formatting.Indented, settings);

Demo fiddle #2 here.

If your value is an enum and you want to serialize it as a string, you can replace NoConverter with StringEnumConverter by using the following:

public class WrapEnumWithValueConverter<TEnum> : JsonConverter where TEnum: Enum
{
    // Here we take advantage of the fact that a converter applied to a property has highest precedence to avoid an infinite recursion.
    class DTO { [JsonConverter(typeof(StringEnumConverter))] public TEnum value { get; set; } public object GetValue() => value; }

    public override bool CanConvert(Type objectType) => typeof(TEnum).IsAssignableFrom(objectType);

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        => serializer.Serialize(writer, new DTO { value = (TEnum)value });

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        => serializer.Deserialize<DTO>(reader)?.GetValue();
}

Demo fiddle #3 here.

Sign up to request clarification or add additional context in comments.

15 Comments

This does seem to work perfectly, however I'm struggling to fully understand the code itself. I'll see if I can figure all this out.
@Daedalus - The converter works by serializing a DTO with the necessary structure. Then there's some extra complexity to prevent the converter from being recursively invoked to serialize the DTO.Value, which would result in a stack overflow exception.
Would this work fine if the "value" is something other than a string, like a bool/decimal/int?
@Daedalus - yes, it was designed to work for any type, just use the correct generic parameter for TValue, e.g. WrapWithValueConverter<bool>, WrapWithValueConverter<decimal>, WrapWithValueConverter<int> etc etc.
Ah okay, so if the returning object contains all of them, the annotation approach would be better?
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.