Reputation: 591
I've enabled my API to serialize/deserialize enum using string values. To do that I've added JsonStringEnumConverter to the list of supported JsonConverters in my API's Startup class:
.AddJsonOptions(opts =>
{
var enumConverter = new JsonStringEnumConverter();
opts.JsonSerializerOptions.Converters.Add(enumConverter);
});
It works fine- my API succeessfully serialize and deserialize enums as a string.
Now- I'm trying to build integration test for my API and having some issue.
I'm using HttpContentJsonExtensions.ReadFromJsonAsync
to deserialize the API response but an exception is thrown over an enum property.
The problem is obvious- HttpContentJsonExtensions.ReadFromJsonAsync
is not aware to the list of converters used by the API (since, as I mentioned earlier, I've added the JsonStringEnumConverter
to the list of supported converters and it works fine).
If I do this in my test function:
var options = new System.Text.Json.JsonSerializerOptions();
options.Converters.Add(new JsonStringEnumConverter());
SomeClass result= await response.Content.ReadFromJsonAsync<SomeClass>(options);
Then the enum property is deserialized and no exception is thrown. However now, ReadFromJsonAsync
is only aware of JsonStringEnumConverter
and not to other JSON converters that are used by the API (like Guid converter)
How can I make sure that HttpContentJsonExtensions.ReadFromJsonAsync
will be able to use all JSON converters that are used by the API?
Thanks!
Upvotes: 3
Views: 1547
Reputation: 63
It worked for me like this
using MvcJsonOptions = Microsoft.AspNetCore.Mvc.JsonOptions;
//used in apis
private static void ConfigureMvcJsonOptions(this IServiceCollection services)
{
services.Configure<MvcJsonOptions>(options =>
{
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
});
}
//-------------------------------//
//used in tests
private static void ConfigureJsonOptions(this IServiceCollection services)
{
//remove registered options before
var jsonOptions = services.Where(o => o.ServiceType == typeof(IConfigureOptions<MvcJsonOptions>))
.ToList();
foreach (var jsonOption in jsonOptions)
{
services.Remove(jsonOption);
}
services.Configure<JsonOptions>(options =>
{
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
});
}
copied idea from https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2293
Upvotes: 0
Reputation: 21
I had the exact same issue when running my integration tests.
I just want to share the workaround I did, which seems simpler to me:
Instead of doing HttpContentJsonExtensions.ReadFromJsonAsync
I read the content as a string first and then deserialize it:
string responseContent = await response.Content.ReadAsStringAsync();
MyClass? actualClass = JsonConvert.DeserializeObject<MyClass>(responseContent);
Using that approach, the enum is correctly deserialized.
Hope this can help someone.
Upvotes: 1
Reputation: 961
Another options is to utilize the options configured in the application.
In my fixture (WebApplicationFactory) constructor I query for the IOptions<JsonOotions>
and store it.
Then either create GetFromJsonAsync
in fixture that creates client and calls endpoint with options or pass them to HttpClient.
Hope it helps
Upvotes: 0
Reputation: 61
I had the same exact issue as described by the OP and my code looked very much the same except I had and Enum and DateOnly converter. I was using the System.Text.Json classes. For me, the problem was that I needed to change another property on the serialization options. I previously just had this:
// Setup custom serializers
var serializerOptions = new JsonSerializerOptions
{
Converters = { new Data.DateOnlyConverter(), new Data.EnumStatusConverter() }
};
I needed to add the following to the serializerOptions object:
serializerOptions.PropertyNameCaseInsensitive = true;
and then my tests could deserialize the values from my PostAsAsync functions.
Upvotes: 1
Reputation: 29869
There is, unfortunately, no way to achieve this out of the box. The reason is that the "designers" of the System.Text.Json APIs decided, in an unbelievably baffling move, to make said APIs static - probably to mimic the well-know Newtonsoft.Json - but static APIs, of course, cannot carry state along with them. For more context I refer you to the feature request to fix this poor design.
I actually came up with a solution in that FR, which I've tweaked a little since I concocted it:
public interface IJsonSerializer
{
JsonSerializerOptions Options { get; }
Task<T> DeserializeAsync<T>(Stream utf8Json, CancellationToken cancellationToken);
// other methods elided for brevity
}
public class DefaultJsonSerializer : IJsonSerializer
{
private JsonSerializerOptions _options;
public JsonSerializerOptions Options
=> new JsonSerializerOptions(_options); // copy constructor so that callers cannot mutate the options
public DefaultJsonSerializer(IOptions<JsonSerializerOptions> options)
=> _options = options.Value;
public async Task<T> DeserializeAsync<T>(Stream utf8Json, CancellationToken cancellationToken = default)
=> await JsonSerializer.DeserializeAsync<T>(utf8Json, Options, cancellationToken);
// other methods elided for brevity
}
Essentially you define a wrapper interface with method signatures identical to the static ones of JsonSerializer
(minus the JsonSerializerOptions
parameter as you want to use the ones defined on the class), then a default implementation of said interface that delegates to the static JsonSerializer
methods. Map the interface to the implementation in Startup.ConfigureServices
and instead of calling the static JsonSerializer
methods in the classes that need to handle JSON, you inject your JSON interface into those classes and use it.
Given that you're using HttpContentJsonExtensions
, you will additionally need to define your own wrapper version of that extension class that duplicates its method signatures but replaces their JsonSerializerOptions
parameters with instances of your JSON serialization interface, then passes said interface's options through to the underlying HttpContentJsonExtensions
implementation:
public static class IJsonSerializerHttpContentJsonExtensions
{
public static Task<object?> ReadFromJsonAsync(this HttpContent content, Type type, IJsonSerializer serializer, CancellationToken cancellationToken = default)
=> HttpContentJsonExtensions.ReadFromJsonAsync(content, type, serializer.Options, cancellationToken);
// other methods elided for brevity
}
Is it painful? Yes. Is it unnecessary? Also yes. Is it stupid? A third time, yes. But such is Microsoft.
Upvotes: 1