Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,8 @@ var gitHubApi = RestService.For<IGitHubApi>("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_.
Expand Down
72 changes: 62 additions & 10 deletions Refit.Tests/SerializedContentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
}
)
)
};

Expand All @@ -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<IPolymorphicRequestApi>(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 { }
Expand All @@ -599,13 +651,13 @@ sealed class TrackingTypeInfoResolver(IJsonTypeInfoResolver innerResolver) : IJs
}
}

sealed class StubHttpMessageHandler(Func<HttpRequestMessage, HttpResponseMessage> responder)
sealed class StubHttpMessageHandler(Func<HttpRequestMessage, Task<HttpResponseMessage>> responder)
: HttpMessageHandler
{
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken
) => Task.FromResult(responder(request));
) => responder(request);
}

sealed class NewtonsoftFieldNameModel
Expand Down
24 changes: 23 additions & 1 deletion Refit/RequestBuilderImplementation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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<T>(IHttpContentSerializer serializer, object? body) =>
serializer.ToHttpContent((T)body!);

IEnumerable<KeyValuePair<string, string?>> ParseQueryParameter(
object? param,
ParameterInfo parameterInfo,
Expand Down
Loading