-1

Let's say I have the following type:

class Foo<T>{
public T data {get;init;}
}

and I want to use this type in multiple controllers:

[HttpPost]
public async void Post(Foo<SomeComplexClass> myFoo){
myFoo.data // this is SomeComplexClass
// Dostuff
}

[HttpPost]
public async void Post2(Foo<OtherMoreDifferentClass>foo2){
foo2.data // this is OtherMoreDifferentClass
}

This is all fine and good, except that the caller of this api which I have no control over serializes T into json before sending the http request, so everything is coming in as Foo<string>. I'm looking into writing a converter for Foo that's able to take this string and automatically convert it to the requested type by deserializing it to the requested type in the JsonConverter.

I'd like to not have to re-write my controllers or put serialization code in my controllers either

The problem is that the JsonConverter<T> class requires that you implement read and write using a utf8jsonreader and writer, which are the lowest level apis in the System.Text.Json library. These deal with individual tokens! All I want to do is take the full string that's coming over the wire, serialize it into a Foo<string> then taking foo.data and re-deserialize it into the requested type if that makes any sense.

Is there a way I can trade this utf8jsonreader for one of the higher level api's somehow? Or is this converter business the wrong approach for this problem?

Update

To clarify, say I have the following class:

public class SomeComplexClass
{
    public string Foo { get; set; } = "";
}

Under normal circumstances, it should be serialized as follows:

{"data":{"Foo":"hello"}}

But the caller of my API is sending the following:

{"data":"{\"Foo\":\"hello\"}"}

As you can seem the value of data has been double-serialized: once to a string, then a second time when the container object is serialized over the wire. I would like to have some automatic way of binding the embedded double-serialized JSON to my Foo<T>.data value.

dbc
  • 104,963
  • 20
  • 228
  • 340
Brandon Piña
  • 614
  • 1
  • 6
  • 20
  • Sorry , but nothing makes any sense to me. – Serge Feb 10 '23 at 22:24
  • 1
    I'm also having trouble understanding your question. Is your problem that your response body (which is, presumably, JSON) contains a `"data"` property that is a string literal containing already-serialized JSON, thus causing your response body to include double-serialized JSON? If so, please [edit] your question to clarify, ideally with a [mcve] showing an example `SomeComplexClass` and raw response body. – dbc Feb 11 '23 at 00:43
  • That would be correct. T is a json encoded string, but I want to parse that json so that I don't have to add parsing code to my controller – Brandon Piña Feb 11 '23 at 02:11

1 Answers1

0

You can create a custom JsonConverter that, in Read(), checks to see whether the current JSON token is a string, and if so, deserializes the string as a string, then deserializes that string to the final data model T. The converter should be applied directly to Foo.data:

class Foo<T>
{
    [JsonConverter(typeof(DoubleSerializedConverter))]
    public T data {get; init;}
}

public class DoubleSerializedConverter : JsonConverterFactory
{
    public override bool CanConvert(Type typeToConvert) => true;

    class ObjectOrArrayKindConverter<T> : JsonConverter<T>
    {
        public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
            reader.TokenType switch
            {
                JsonTokenType.String => JsonSerializer.Deserialize<T>(reader.GetString()!, options),
                _ => JsonSerializer.Deserialize<T>(ref reader, options),
            };
        public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => JsonSerializer.Serialize(writer, value, options);
    }
    
    class UnknownKindConverter<T> : JsonConverter<T>
    {
        // TODO: decide how to handle primitive types like string, byte [] and so on.  There's no foolproof way to check whether they were conditionally double serialized,
        // so you must either assume they were always double-serialized, or assume they were never double-serialized.
        //public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => JsonSerializer.Deserialize<T>(ref reader, options);
        public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => 
            reader.TokenType switch
            {
                JsonTokenType.String => JsonSerializer.Deserialize<T>(reader.GetString()!, options), // Use if you want to assume strings and other primitives are double serialized.
                //JsonTokenType.String => JsonSerializer.Deserialize<T>(ref reader, options),  // Use if you want to assume strings and other primitives are not double serialized.
                _ => JsonSerializer.Deserialize<T>(ref reader, options),
            };
        public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => JsonSerializer.Serialize(writer, value, options);
    }

    public sealed override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
    {
        var typeInfo = JsonSerializerOptions.Default.GetTypeInfo(typeToConvert);
        var genericConverterType = (typeInfo.Kind == JsonTypeInfoKind.None ? typeof(UnknownKindConverter<>) : typeof(ObjectOrArrayKindConverter<>));
        return (JsonConverter)Activator.CreateInstance(genericConverterType.MakeGenericType(typeToConvert))!;
    }
}

Notes:

  • It is possible to reliably check whether a POCO that is normally serialized as a JSON object or array was double-serialized, but it is impossible to reliably check whether a .NET primitive was double serialized. E.g. the raw JSON string "\"hello\"" might be a doubly-serialized hello or a singly-serialized "hello".

    Thus you will need to decide what to do for primitives. The converter above assumes primitives are double-serialized, but if you would prefer the opposite, in UnknownKindConverter<T> above uncomment

    //JsonTokenType.String => JsonSerializer.Deserialize<T>(ref reader, options)` 
    
  • JsonSerializerOptions.GetTypeInfo(Type) is used to determine whether the T data is normally serialized as an JSON object or array. This API was introduced in .NET 7 so if you are working in an earlier version you may need to replace that logic with something handcrafted.

Demo fiddle here

dbc
  • 104,963
  • 20
  • 228
  • 340