Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save martincostello/53b10574fbee2d8bc7b0f75bf0855a84 to your computer and use it in GitHub Desktop.
Save martincostello/53b10574fbee2d8bc7b0f75bf0855a84 to your computer and use it in GitHub Desktop.
A class for plugging the JSON source generator with Refit
using System.Net;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Refit;
/// <summary>
/// A class representing an implementation of <see cref="IHttpContentSerializer"/> that can be
/// used with the <c>System.Text.Json</c> source generators. This class cannot be inherited.
/// </summary>
/// <param name="jsonSerializerContext">The <see cref="JsonSerializerContext"/> to use.</param>
public sealed class SystemTextJsonContentSerializerForSourceGenerator(JsonSerializerContext jsonSerializerContext) : IHttpContentSerializer
{
//// Based on https://github.com/reactiveui/refit/blob/main/Refit/SystemTextJsonContentSerializer.cs
private readonly JsonSerializerContext _jsonSerializerContext = jsonSerializerContext;
/// <inheritdoc />
public HttpContent ToHttpContent<T>(T item)
{
ArgumentNullException.ThrowIfNull(item);
#if NET8_0_OR_GREATER
var jsonTypeInfo = _jsonSerializerContext.GetTypeInfo(typeof(T));
return System.Net.Http.Json.JsonContent.Create(item, jsonTypeInfo);
#else
return new JsonContent(item, typeof(T), _jsonSerializerContext);
#endif
}
/// <inheritdoc />
public async Task<T?> FromHttpContentAsync<T>(HttpContent content, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(content);
using var stream = await content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
#if NET8_0_OR_GREATER
var jsonTypeInfo = _jsonSerializerContext.GetTypeInfo(typeof(T)) as JsonTypeInfo<T>;
return await JsonSerializer.DeserializeAsync(stream, jsonTypeInfo, cancellationToken).ConfigureAwait(false);
#else
object result = await JsonSerializer.DeserializeAsync(
stream,
typeof(T),
_jsonSerializerContext,
cancellationToken).ConfigureAwait(false);
return (T?)result;
#endif
}
/// <inheritdoc />
public string? GetFieldNameForProperty(PropertyInfo propertyInfo)
{
ArgumentNullException.ThrowIfNull(propertyInfo);
return propertyInfo
.GetCustomAttributes<JsonPropertyNameAttribute>(true)
.Select(x => x.Name)
.FirstOrDefault();
}
#if !NET8_0_OR_GREATER
/// <summary>
/// Based on https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs.
/// </summary>
private sealed class JsonContent : HttpContent
{
private readonly object _value;
private readonly Type _objectType;
private readonly JsonSerializerContext _serializerContext;
internal JsonContent(object inputValue, Type inputType, JsonSerializerContext context)
{
_value = inputValue;
_objectType = inputType;
_serializerContext = context;
Headers.ContentType = new("application/json") { CharSet = "utf-8" };
}
protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context)
{
return JsonSerializer.SerializeAsync(stream, _value, _objectType, _serializerContext, CancellationToken.None);
}
protected override bool TryComputeLength(out long length)
{
length = 0;
return false;
}
}
#endif
}
@djeikyb
Copy link

djeikyb commented Feb 6, 2024

Thanks for this example! I tried it out with net8.0 and replaced ToHttpContent like:

public HttpContent ToHttpContent<T>(T item)
{
    ArgumentNullException.ThrowIfNull(item);
    return JsonContent.Create(item, typeof(T), options: _jsonSerializerContext.Options);
}

And deleted the JsonContent implementation. Then used it like:

services.AddRefitClient<IGitLabApi>(
    new RefitSettings
        {
            ContentSerializer =
                new SystemTextJsonContentSerializerForSourceGenerator(GitlabJsonContext.Default)
        }
)

@Digifais
Copy link

@martincostello Would you be able to give me an example of your JsonSerializerContext?

Currently running into problems after migrating my app from Xamarin.iOS to .NET for iOS with my API calls using Refit. I've already created my own JsonSerializerContext sub-class, but adding a JsonSerializable attribute for every type that Refit needs to serialize/deserialize to, feels very obnoxious to me. Am I missing something?

@martincostello
Copy link
Author

I've stopped using Refit in my own code as it's not AoT-friendly due to its use of Reflection, but you can find examples of my JsonSerializerContext classes with this GitHub code search link.

You'll need to declare as many types with attributes as are required for the source generator to be able to recursively go through the definition(s) to find every type as that's how the JSON source generator is designed to work as it needs to know ahead of time what you're going to need at runtime to generate the appropriate code. For example, if you had a class that had a property of every other type you (de)serialize, then you would only need to add an attribute for that type as it would by extension have to include those types to be able to handle that parent class. Similarly, if you serialize a T[], then you only need to add [JsonSerializabletypeof(T[])] as by extension that includes typeof(T).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment