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!

dbc
  • 104,963
  • 20
  • 228
  • 340
Daedalus
  • 3,632
  • 4
  • 14
  • 18
  • 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. – Jonesopolis May 06 '21 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. – Daedalus May 06 '21 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. – Jonesopolis May 06 '21 at 15:14

1 Answers1

5

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.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • 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 May 06 '21 at 19:36
  • @Daedalus - The converter works by serializing a [DTO](https://en.wikipedia.org/wiki/Data_transfer_object) 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. – dbc May 06 '21 at 20:04
  • Would this work fine if the "value" is something other than a string, like a bool/decimal/int? – Daedalus May 06 '21 at 20:57
  • @Daedalus - yes, it was designed to work for any type, just use the correct generic parameter for `TValue`, e.g. `WrapWithValueConverter`, `WrapWithValueConverter`, `WrapWithValueConverter` etc etc. – dbc May 06 '21 at 21:38
  • Ah okay, so if the returning object contains all of them, the annotation approach would be better? – Daedalus May 07 '21 at 02:53
  • Or you could add the multiple converters (`WrapWithValueConverter`, `WrapWithValueConverter`, `WrapWithValueConverter` and so on) to settings. Or maybe you should be looking at a custom contract resolver to apply the converter(s) as required, a contract resolver can be a good way to apply wholescale transforms to your JSON. See: [JSON.net ContractResolver vs. JsonConverter](https://stackoverflow.com/a/41094764/3744182). Can't say without a [mcve] so maybe you should ask a second question? – dbc May 07 '21 at 03:34
  • This is actually working perfectly, except for one edge case... I am working with Enum's, using the [EnumMember(Value = "X")] annotation. I've got it properly converting to the Member value with [JsonConverter(typeof(StringEnumConverter))], however its not being handled by the WrapWithValueConverter upon serialization. – Daedalus May 24 '21 at 19:22
  • *I've got it properly converting to the Member value with [JsonConverter(typeof(StringEnumConverter))]* - How are you applying `[JsonConverter(typeof(StringEnumConverter))]`? Is it in settings? – dbc May 24 '21 at 19:53
  • I've added that annotation to the class member directly. I'm passing in the WrapWithValueConverter into the converter settings in DeserializeObject call. My apologies, I meant Deserialization. Converting the object to a string. This specifically is Customer Status. The actual value that sits in JSON will be "A", but the Enum value is "ACTIVE". – Daedalus May 24 '21 at 19:56
  • *I've added that annotation to the class member* - you mean you applied it directly to the `enum` type, or to some field or property? Can you share a [mcve]? – dbc May 24 '21 at 19:58
  • I can provide a MRE, but it'll take me a bit. In the meantime: I have `public enum CustomerStatus { [EnumMember(Value = "A")] ACTIVE }`. Then in my class I have `public class Customer { [JsonConverter(typeof(StringEnumConverter))] public CustomerStatus Status { get; set;} }`. Upon Deserialization, I do get the value "A", however its not wrapped with value. – Daedalus May 24 '21 at 20:09
  • I'm almost wondering if this is even worth the trouble, and if I should just make the datatype a string. I have a feeling this is more complicated than it realistically needs to be. – Daedalus May 24 '21 at 20:16
  • That's because converters applied to a property supersede converters applied in settings. I mentioned that in the answer: *`// Here we take advantage of the fact that a converter applied to a property has highest precedence to avoid an infinite recursion.`*. For confirmation see [C# JsonConvert using the default converter instead of the custom converter](https://stackoverflow.com/q/53401189/3744182). If you need nested converters for your member you may need to apply the decorator pattern. A [mcve] (or even a separate question) would help here. – dbc May 24 '21 at 20:16
  • @Daedalus - You could add `WrapEnumWithValueConverter` for enums that applies `StringEnumConverter` rather than `NoConverter` to the nested value. See https://dotnetfiddle.net/cTFGUj. (If you also need to support nullable enums you might need to enhance it a bit.) – dbc May 24 '21 at 20:23
  • This worked perfectly. Thank you so much, I really appreciate the support! – Daedalus May 24 '21 at 20:37