diff --git a/README.md b/README.md index 62f8c755b..fcd477daa 100644 --- a/README.md +++ b/README.md @@ -513,6 +513,8 @@ var gitHubApi = RestService.For("https://api.github.com", }); ``` +When using `System.Text.Json` polymorphism features such as `[JsonDerivedType]` / `[JsonPolymorphic]`, Refit serializes request bodies using the **declared Refit method parameter type** rather than the boxed runtime `object`. This ensures type discriminators configured on the base contract are preserved in outgoing request payloads. + #### XML Content XML requests and responses are serialized/deserialized using _System.Xml.Serialization.XmlSerializer_. diff --git a/Refit.Tests/SerializedContentTests.cs b/Refit.Tests/SerializedContentTests.cs index ad2bff732..b09f50887 100644 --- a/Refit.Tests/SerializedContentTests.cs +++ b/Refit.Tests/SerializedContentTests.cs @@ -560,14 +560,16 @@ public async Task RestService_CanUseSourceGeneratedSystemTextJsonMetadata() ) { HttpMessageHandlerFactory = () => new StubHttpMessageHandler( - _ => new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent( - "{\"name\":\"Road Runner\",\"company\":\"ACME\",\"createdAt\":\"1949-09-17\"}", - Encoding.UTF8, - "application/json" - ) - } + _ => Task.FromResult( + new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent( + "{\"name\":\"Road Runner\",\"company\":\"ACME\",\"createdAt\":\"1949-09-17\"}", + Encoding.UTF8, + "application/json" + ) + } + ) ) }; @@ -581,9 +583,59 @@ public async Task RestService_CanUseSourceGeneratedSystemTextJsonMetadata() Assert.Contains(typeof(User), resolver.RequestedTypes); } + [Fact] + public async Task RestService_SerializesBodyUsingDeclaredPolymorphicBaseType() + { + string? serializedBody = null; + var settings = new RefitSettings( + new SystemTextJsonContentSerializer( + new JsonSerializerOptions(JsonSerializerDefaults.Web) + { + TypeInfoResolver = PolymorphicRequestJsonSerializerContext.Default + } + ) + ) + { + HttpMessageHandlerFactory = () => new StubHttpMessageHandler(async request => + { + serializedBody = await request.Content!.ReadAsStringAsync(); + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("{}", Encoding.UTF8, "application/json") + }; + }) + }; + + var api = RestService.For(BaseAddress, settings); + await api.CreateWeapon(new LaserWeaponRequest { Name = "Photon" }); + + Assert.NotNull(serializedBody); + Assert.Contains("\"$type\":\"laser\"", serializedBody, StringComparison.Ordinal); + Assert.Contains("\"name\":\"Photon\"", serializedBody, StringComparison.Ordinal); + } + + [JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] + [JsonDerivedType(typeof(LaserWeaponRequest), "laser")] + public abstract class CreateWeaponRequest + { + public string? Name { get; set; } + } + + public sealed class LaserWeaponRequest : CreateWeaponRequest { } + + public interface IPolymorphicRequestApi + { + [Post("/weapons")] + Task CreateWeapon(CreateWeaponRequest request); + } + [JsonSerializable(typeof(User))] internal sealed partial class SerializedContentJsonSerializerContext : JsonSerializerContext { } + [JsonSerializable(typeof(CreateWeaponRequest))] + [JsonSerializable(typeof(LaserWeaponRequest))] + internal sealed partial class PolymorphicRequestJsonSerializerContext : JsonSerializerContext { } + [JsonSerializable(typeof(ObjectValueContainer))] [JsonSerializable(typeof(string))] internal sealed partial class ObjectValueContainerJsonSerializerContext : JsonSerializerContext { } @@ -599,13 +651,13 @@ sealed class TrackingTypeInfoResolver(IJsonTypeInfoResolver innerResolver) : IJs } } - sealed class StubHttpMessageHandler(Func responder) + sealed class StubHttpMessageHandler(Func> responder) : HttpMessageHandler { protected override Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken - ) => Task.FromResult(responder(request)); + ) => responder(request); } sealed class NewtonsoftFieldNameModel diff --git a/Refit/RequestBuilderImplementation.cs b/Refit/RequestBuilderImplementation.cs index 3e474050d..fc72ec29a 100644 --- a/Refit/RequestBuilderImplementation.cs +++ b/Refit/RequestBuilderImplementation.cs @@ -1003,7 +1003,10 @@ void AddBodyToRequest(RestMethodInfoInternal restMethod, object param, HttpReque case BodySerializationMethod.Json: #pragma warning restore CS0618 // Type or member is obsolete case BodySerializationMethod.Serialized: - var content = serializer.ToHttpContent(param); + var declaredBodyType = restMethod.ParameterInfoArray[ + restMethod.BodyParameterInfo.Item3 + ].ParameterType; + var content = SerializeBody(serializer, param, declaredBodyType); switch (restMethod.BodyParameterInfo.Item2) { case false: @@ -1200,6 +1203,25 @@ void AddVersionToRequest(HttpRequestMessage ret) } #endif + static readonly MethodInfo SerializeBodyMethod = + typeof(RequestBuilderImplementation).GetMethod( + nameof(SerializeBodyGeneric), + BindingFlags.Static | BindingFlags.NonPublic + )!; + + static HttpContent SerializeBody( + IHttpContentSerializer serializer, + object? body, + Type declaredBodyType + ) + { + var serializeMethod = SerializeBodyMethod.MakeGenericMethod(declaredBodyType); + return (HttpContent)serializeMethod.Invoke(null, [serializer, body])!; + } + + static HttpContent SerializeBodyGeneric(IHttpContentSerializer serializer, object? body) => + serializer.ToHttpContent((T)body!); + IEnumerable> ParseQueryParameter( object? param, ParameterInfo parameterInfo,