Skip to content

Commit d33cbf8

Browse files
authored
[csharp] Fix oneOf/anyOf response deserialization in httpclient library (#25080)
* [csharp] Fix oneOf/anyOf response deserialization in httpclient library In the httpclient library's ApiClient.ExecAsync, responses whose schema is oneOf/anyOf (types derived from AbstractOpenAPISchema) are deserialized by reflectively invoking the model's static FromJson(string) method. The call passed response.Content (an HttpContent) instead of the JSON string FromJson expects. Because the invocation is reflective it compiled but threw ArgumentException at runtime ("object of type HttpConnectionResponseContent cannot be converted to type String") on every oneOf/anyOf response. Read the response body as a string first and pass that to FromJson. The content is already buffered by the preceding Deserialize call, so re-reading it is safe. Regenerated the affected csharp httpclient samples. Fixes #25079 * [csharp] Add regression test for oneOf/anyOf httpclient response deserialization Adds a unit test to the net10, net9 and standard2.0 httpclient Petstore samples that drives ApiClient.GetAsync<Pig> (Pig is a oneOf) through a stub HttpMessageHandler and asserts the response is deserialized to the correct concrete type. Without the fix this throws the ArgumentException reported in #25079. Refs #25079 * [csharp] Dispose HttpClient/ApiClient in oneOf regression test ApiClient does not own an injected HttpClient, so the test must dispose it itself. Use 'using' declarations for both the HttpClient and ApiClient. Addresses review feedback on #25080.
1 parent ba7fb8b commit d33cbf8

9 files changed

Lines changed: 219 additions & 6 deletions

File tree

‎modules/openapi-generator/src/main/resources/csharp/libraries/httpclient/ApiClient.mustache‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -563,7 +563,8 @@ namespace {{packageName}}.Client
563563
// if the response type is oneOf/anyOf, call FromJSON to deserialize the data
564564
if (typeof({{{packageName}}}.{{modelPackage}}.AbstractOpenAPISchema).IsAssignableFrom(typeof(T)))
565565
{
566-
responseData = (T)typeof(T).GetMethod("FromJson").Invoke(null, new object[] { response.Content });
566+
string jsonString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
567+
responseData = (T)typeof(T).GetMethod("FromJson").Invoke(null, new object[] { jsonString });
567568
}
568569
else if (typeof(T).Name == "Stream") // for binary response
569570
{

‎samples/client/petstore/csharp/httpclient/net10/Petstore-nonPublicApi/src/Org.OpenAPITools/Client/ApiClient.cs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -546,7 +546,8 @@ private async Task<ApiResponse<T>> ExecAsync<T>(HttpRequestMessage req,
546546
// if the response type is oneOf/anyOf, call FromJSON to deserialize the data
547547
if (typeof(Org.OpenAPITools.Model.AbstractOpenAPISchema).IsAssignableFrom(typeof(T)))
548548
{
549-
responseData = (T)typeof(T).GetMethod("FromJson").Invoke(null, new object[] { response.Content });
549+
string jsonString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
550+
responseData = (T)typeof(T).GetMethod("FromJson").Invoke(null, new object[] { jsonString });
550551
}
551552
else if (typeof(T).Name == "Stream") // for binary response
552553
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* OpenAPI Petstore
3+
*
4+
* This spec is mainly for testing Petstore server and contains fake endpoints, models. Please do not use this for any other purpose. Special characters: \" \\
5+
*
6+
* The version of the OpenAPI document: 1.0.0
7+
* Generated by: https://github.com/openapitools/openapi-generator.git
8+
*/
9+
10+
using System.Net;
11+
using System.Net.Http;
12+
using System.Text;
13+
using System.Threading;
14+
using System.Threading.Tasks;
15+
using Org.OpenAPITools.Client;
16+
using Org.OpenAPITools.Model;
17+
using Xunit;
18+
19+
namespace Org.OpenAPITools.Test.Client
20+
{
21+
/// <summary>
22+
/// Regression tests for deserializing oneOf/anyOf responses (types derived from
23+
/// AbstractOpenAPISchema) in the httpclient library.
24+
/// See https://github.com/OpenAPITools/openapi-generator/issues/25079
25+
/// </summary>
26+
public class OneOfResponseDeserializationTests
27+
{
28+
/// <summary>
29+
/// Returns a canned JSON body for every request, so ExecAsync can be exercised
30+
/// without a live server.
31+
/// </summary>
32+
private sealed class StubHttpMessageHandler : HttpMessageHandler
33+
{
34+
private readonly string _json;
35+
36+
public StubHttpMessageHandler(string json)
37+
{
38+
_json = json;
39+
}
40+
41+
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
42+
{
43+
var response = new HttpResponseMessage(HttpStatusCode.OK)
44+
{
45+
Content = new StringContent(_json, Encoding.UTF8, "application/json")
46+
};
47+
return Task.FromResult(response);
48+
}
49+
}
50+
51+
/// <summary>
52+
/// A oneOf/anyOf response must be deserialized via the model's static
53+
/// FromJson(string) method. Previously ApiClient.ExecAsync passed the
54+
/// HttpContent instead of the body string, which threw an ArgumentException
55+
/// at runtime for every oneOf/anyOf response.
56+
/// </summary>
57+
[Fact]
58+
public async Task OneOfResponseIsDeserializedToConcreteType()
59+
{
60+
using var httpClient = new HttpClient(new StubHttpMessageHandler("{\"className\":\"BasquePig\"}"));
61+
using var apiClient = new ApiClient(httpClient, "http://localhost");
62+
63+
ApiResponse<Pig> response = await apiClient.GetAsync<Pig>("/fake", new RequestOptions());
64+
65+
Assert.NotNull(response.Data);
66+
Assert.IsType<BasquePig>(response.Data.ActualInstance);
67+
}
68+
}
69+
}

‎samples/client/petstore/csharp/httpclient/net10/Petstore/src/Org.OpenAPITools/Client/ApiClient.cs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -546,7 +546,8 @@ private async Task<ApiResponse<T>> ExecAsync<T>(HttpRequestMessage req,
546546
// if the response type is oneOf/anyOf, call FromJSON to deserialize the data
547547
if (typeof(Org.OpenAPITools.Model.AbstractOpenAPISchema).IsAssignableFrom(typeof(T)))
548548
{
549-
responseData = (T)typeof(T).GetMethod("FromJson").Invoke(null, new object[] { response.Content });
549+
string jsonString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
550+
responseData = (T)typeof(T).GetMethod("FromJson").Invoke(null, new object[] { jsonString });
550551
}
551552
else if (typeof(T).Name == "Stream") // for binary response
552553
{

‎samples/client/petstore/csharp/httpclient/net9/Petstore-nonPublicApi/src/Org.OpenAPITools/Client/ApiClient.cs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -546,7 +546,8 @@ private async Task<ApiResponse<T>> ExecAsync<T>(HttpRequestMessage req,
546546
// if the response type is oneOf/anyOf, call FromJSON to deserialize the data
547547
if (typeof(Org.OpenAPITools.Model.AbstractOpenAPISchema).IsAssignableFrom(typeof(T)))
548548
{
549-
responseData = (T)typeof(T).GetMethod("FromJson").Invoke(null, new object[] { response.Content });
549+
string jsonString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
550+
responseData = (T)typeof(T).GetMethod("FromJson").Invoke(null, new object[] { jsonString });
550551
}
551552
else if (typeof(T).Name == "Stream") // for binary response
552553
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* OpenAPI Petstore
3+
*
4+
* This spec is mainly for testing Petstore server and contains fake endpoints, models. Please do not use this for any other purpose. Special characters: \" \\
5+
*
6+
* The version of the OpenAPI document: 1.0.0
7+
* Generated by: https://github.com/openapitools/openapi-generator.git
8+
*/
9+
10+
using System.Net;
11+
using System.Net.Http;
12+
using System.Text;
13+
using System.Threading;
14+
using System.Threading.Tasks;
15+
using Org.OpenAPITools.Client;
16+
using Org.OpenAPITools.Model;
17+
using Xunit;
18+
19+
namespace Org.OpenAPITools.Test.Client
20+
{
21+
/// <summary>
22+
/// Regression tests for deserializing oneOf/anyOf responses (types derived from
23+
/// AbstractOpenAPISchema) in the httpclient library.
24+
/// See https://github.com/OpenAPITools/openapi-generator/issues/25079
25+
/// </summary>
26+
public class OneOfResponseDeserializationTests
27+
{
28+
/// <summary>
29+
/// Returns a canned JSON body for every request, so ExecAsync can be exercised
30+
/// without a live server.
31+
/// </summary>
32+
private sealed class StubHttpMessageHandler : HttpMessageHandler
33+
{
34+
private readonly string _json;
35+
36+
public StubHttpMessageHandler(string json)
37+
{
38+
_json = json;
39+
}
40+
41+
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
42+
{
43+
var response = new HttpResponseMessage(HttpStatusCode.OK)
44+
{
45+
Content = new StringContent(_json, Encoding.UTF8, "application/json")
46+
};
47+
return Task.FromResult(response);
48+
}
49+
}
50+
51+
/// <summary>
52+
/// A oneOf/anyOf response must be deserialized via the model's static
53+
/// FromJson(string) method. Previously ApiClient.ExecAsync passed the
54+
/// HttpContent instead of the body string, which threw an ArgumentException
55+
/// at runtime for every oneOf/anyOf response.
56+
/// </summary>
57+
[Fact]
58+
public async Task OneOfResponseIsDeserializedToConcreteType()
59+
{
60+
using var httpClient = new HttpClient(new StubHttpMessageHandler("{\"className\":\"BasquePig\"}"));
61+
using var apiClient = new ApiClient(httpClient, "http://localhost");
62+
63+
ApiResponse<Pig> response = await apiClient.GetAsync<Pig>("/fake", new RequestOptions());
64+
65+
Assert.NotNull(response.Data);
66+
Assert.IsType<BasquePig>(response.Data.ActualInstance);
67+
}
68+
}
69+
}

‎samples/client/petstore/csharp/httpclient/net9/Petstore/src/Org.OpenAPITools/Client/ApiClient.cs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -546,7 +546,8 @@ private async Task<ApiResponse<T>> ExecAsync<T>(HttpRequestMessage req,
546546
// if the response type is oneOf/anyOf, call FromJSON to deserialize the data
547547
if (typeof(Org.OpenAPITools.Model.AbstractOpenAPISchema).IsAssignableFrom(typeof(T)))
548548
{
549-
responseData = (T)typeof(T).GetMethod("FromJson").Invoke(null, new object[] { response.Content });
549+
string jsonString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
550+
responseData = (T)typeof(T).GetMethod("FromJson").Invoke(null, new object[] { jsonString });
550551
}
551552
else if (typeof(T).Name == "Stream") // for binary response
552553
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* OpenAPI Petstore
3+
*
4+
* This spec is mainly for testing Petstore server and contains fake endpoints, models. Please do not use this for any other purpose. Special characters: \" \\
5+
*
6+
* The version of the OpenAPI document: 1.0.0
7+
* Generated by: https://github.com/openapitools/openapi-generator.git
8+
*/
9+
10+
using System.Net;
11+
using System.Net.Http;
12+
using System.Text;
13+
using System.Threading;
14+
using System.Threading.Tasks;
15+
using Org.OpenAPITools.Client;
16+
using Org.OpenAPITools.Model;
17+
using Xunit;
18+
19+
namespace Org.OpenAPITools.Test.Client
20+
{
21+
/// <summary>
22+
/// Regression tests for deserializing oneOf/anyOf responses (types derived from
23+
/// AbstractOpenAPISchema) in the httpclient library.
24+
/// See https://github.com/OpenAPITools/openapi-generator/issues/25079
25+
/// </summary>
26+
public class OneOfResponseDeserializationTests
27+
{
28+
/// <summary>
29+
/// Returns a canned JSON body for every request, so ExecAsync can be exercised
30+
/// without a live server.
31+
/// </summary>
32+
private sealed class StubHttpMessageHandler : HttpMessageHandler
33+
{
34+
private readonly string _json;
35+
36+
public StubHttpMessageHandler(string json)
37+
{
38+
_json = json;
39+
}
40+
41+
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
42+
{
43+
var response = new HttpResponseMessage(HttpStatusCode.OK)
44+
{
45+
Content = new StringContent(_json, Encoding.UTF8, "application/json")
46+
};
47+
return Task.FromResult(response);
48+
}
49+
}
50+
51+
/// <summary>
52+
/// A oneOf/anyOf response must be deserialized via the model's static
53+
/// FromJson(string) method. Previously ApiClient.ExecAsync passed the
54+
/// HttpContent instead of the body string, which threw an ArgumentException
55+
/// at runtime for every oneOf/anyOf response.
56+
/// </summary>
57+
[Fact]
58+
public async Task OneOfResponseIsDeserializedToConcreteType()
59+
{
60+
using var httpClient = new HttpClient(new StubHttpMessageHandler("{\"className\":\"BasquePig\"}"));
61+
using var apiClient = new ApiClient(httpClient, "http://localhost");
62+
63+
ApiResponse<Pig> response = await apiClient.GetAsync<Pig>("/fake", new RequestOptions());
64+
65+
Assert.NotNull(response.Data);
66+
Assert.IsType<BasquePig>(response.Data.ActualInstance);
67+
}
68+
}
69+
}

‎samples/client/petstore/csharp/httpclient/standard2.0/Petstore/src/Org.OpenAPITools/Client/ApiClient.cs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -545,7 +545,8 @@ private async Task<ApiResponse<T>> ExecAsync<T>(HttpRequestMessage req,
545545
// if the response type is oneOf/anyOf, call FromJSON to deserialize the data
546546
if (typeof(Org.OpenAPITools.Model.AbstractOpenAPISchema).IsAssignableFrom(typeof(T)))
547547
{
548-
responseData = (T)typeof(T).GetMethod("FromJson").Invoke(null, new object[] { response.Content });
548+
string jsonString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
549+
responseData = (T)typeof(T).GetMethod("FromJson").Invoke(null, new object[] { jsonString });
549550
}
550551
else if (typeof(T).Name == "Stream") // for binary response
551552
{

0 commit comments

Comments
 (0)