Skip to content

Commit bf24794

Browse files
Add AddJsonBody overload to serialise top-level string (#2043)
* Added AddJsonBody overload for top-level strings
1 parent 2d42a33 commit bf24794

File tree

9 files changed

+123
-48
lines changed

9 files changed

+123
-48
lines changed

docs/usage.md

+24-4
Original file line numberDiff line numberDiff line change
@@ -339,17 +339,37 @@ When you call `AddJsonBody`, it does the following for you:
339339
- Sets the content type to `application/json`
340340
- Sets the internal data type of the request body to `DataType.Json`
341341

342-
::: warning
343-
Do not send JSON string or some sort of `JObject` instance to `AddJsonBody`; it won't work! Use `AddStringBody` instead.
344-
:::
345-
346342
Here is the example:
347343

348344
```csharp
349345
var param = new MyClass { IntData = 1, StringData = "test123" };
350346
request.AddJsonBody(param);
351347
```
352348

349+
It is possible to override the default content type by supplying the `contentType` argument. For example:
350+
351+
```csharp
352+
request.AddJsonBody(param, "text/x-json");
353+
```
354+
355+
If you use a pre-serialized string with `AddJsonBody`, it will be sent as-is. The `AddJsonBody` will detect if the parameter is a string and will add it as a string body with JSON content type.
356+
Essentially, it means that top-level strings won't be serialized as JSON when you use `AddJsonBody`. To overcome this issue, you can use an overload of `AddJsonBody`, which allows you to tell RestSharp to serialize the string as JSON:
357+
358+
```csharp
359+
const string payload = @"
360+
""requestBody"": {
361+
""content"": {
362+
""application/json"": {
363+
""schema"": {
364+
""type"": ""string""
365+
}
366+
}
367+
}
368+
},";
369+
request.AddJsonBody(payload, forceSerialize: true); // the string will be serialized
370+
request.AddJsonBody(payload); // the string will NOT be serialized and will be sent as-is
371+
```
372+
353373
#### AddXmlBody
354374

355375
When you call `AddXmlBody`, it does the following for you:

src/RestSharp.Serializers.NewtonsoftJson/JsonNetSerializer.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public class JsonNetSerializer : IRestSerializer, ISerializer, IDeserializer {
7373
public ISerializer Serializer => this;
7474
public IDeserializer Deserializer => this;
7575

76-
public string[] AcceptedContentTypes => RestSharp.ContentType.JsonAccept;
76+
public string[] AcceptedContentTypes => ContentType.JsonAccept;
7777

7878
public ContentType ContentType { get; set; } = ContentType.Json;
7979

src/RestSharp/Extensions/StringExtensions.cs

+3-5
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
using System.Diagnostics.CodeAnalysis;
1516
using System.Globalization;
1617
using System.Text;
1718
using System.Text.RegularExpressions;
@@ -144,12 +145,9 @@ internal static IEnumerable<string> GetNameVariants(this string name, CultureInf
144145
yield return name.AddSpaces().ToLower(culture);
145146
}
146147

147-
internal static bool IsEmpty(this string? value) => string.IsNullOrWhiteSpace(value);
148+
internal static bool IsEmpty([NotNullWhen(false)] this string? value) => string.IsNullOrWhiteSpace(value);
148149

149-
internal static bool IsNotEmpty(this string? value) => !string.IsNullOrWhiteSpace(value);
150-
151-
internal static string JoinToString<T>(this IEnumerable<T> collection, string separator, Func<T, string> getString)
152-
=> JoinToString(collection.Select(getString), separator);
150+
internal static bool IsNotEmpty([NotNullWhen(true)] this string? value) => !string.IsNullOrWhiteSpace(value);
153151

154152
internal static string JoinToString(this IEnumerable<string> strings, string separator) => string.Join(separator, strings);
155153

src/RestSharp/Request/BodyExtensions.cs

+2-4
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,15 @@
1313
// limitations under the License.
1414
//
1515

16-
namespace RestSharp;
16+
namespace RestSharp;
1717

1818
using System.Diagnostics.CodeAnalysis;
1919

2020
static class BodyExtensions {
21-
public static bool TryGetBodyParameter(this RestRequest request, out BodyParameter? bodyParameter) {
21+
public static bool TryGetBodyParameter(this RestRequest request, [NotNullWhen(true)] out BodyParameter? bodyParameter) {
2222
bodyParameter = request.Parameters.FirstOrDefault(p => p.Type == ParameterType.RequestBody) as BodyParameter;
2323
return bodyParameter != null;
2424
}
2525

2626
public static bool HasFiles(this RestRequest request) => request.Files.Count > 0;
27-
28-
public static bool IsEmpty([NotNullWhen(false)]this ParametersCollection? parameters) => parameters == null || parameters.Count == 0;
2927
}

src/RestSharp/Request/RequestContent.cs

+4-8
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,7 @@ void AddFiles() {
7272

7373
HttpContent Serialize(BodyParameter body) {
7474
return body.DataFormat switch {
75-
DataFormat.None => new StringContent(
76-
body.Value!.ToString()!,
77-
_client.Options.Encoding,
78-
body.ContentType.Value
79-
),
75+
DataFormat.None => new StringContent(body.Value!.ToString()!, _client.Options.Encoding, body.ContentType.Value),
8076
DataFormat.Binary => GetBinary(),
8177
_ => GetSerialized()
8278
};
@@ -124,19 +120,19 @@ MultipartFormDataContent CreateMultipartFormDataContent() {
124120
void AddBody(bool hasPostParameters) {
125121
if (!_request.TryGetBodyParameter(out var bodyParameter)) return;
126122

127-
var bodyContent = Serialize(bodyParameter!);
123+
var bodyContent = Serialize(bodyParameter);
128124

129125
// we need to send the body
130126
if (hasPostParameters || _request.HasFiles() || BodyShouldBeMultipartForm(bodyParameter!) || _request.AlwaysMultipartFormData) {
131127
// here we must use multipart form data
132128
var mpContent = Content as MultipartFormDataContent ?? CreateMultipartFormDataContent();
133129
var ct = bodyContent.Headers.ContentType?.MediaType;
134-
var name = bodyParameter!.Name.IsEmpty() ? ct : bodyParameter.Name;
130+
var name = bodyParameter.Name.IsEmpty() ? ct : bodyParameter.Name;
135131

136132
if (name.IsEmpty())
137133
mpContent.Add(bodyContent);
138134
else
139-
mpContent.Add(bodyContent, name!);
135+
mpContent.Add(bodyContent, name);
140136
Content = mpContent;
141137
}
142138
else {

src/RestSharp/Request/RestRequestExtensions.cs

+25-6
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ public static RestRequest AddBody(this RestRequest request, object obj, ContentT
338338
DataFormat.Json => request.AddJsonBody(obj, contentType),
339339
DataFormat.Xml => request.AddXmlBody(obj, contentType),
340340
DataFormat.Binary => request.AddParameter(new BodyParameter("", obj, ContentType.Binary)),
341-
_ => request.AddParameter(new BodyParameter("", obj.ToString()!, ContentType.Plain))
341+
_ => request.AddParameter(new BodyParameter("", obj.ToString(), ContentType.Plain))
342342
};
343343
}
344344

@@ -374,6 +374,22 @@ public static RestRequest AddStringBody(this RestRequest request, string body, D
374374
public static RestRequest AddStringBody(this RestRequest request, string body, ContentType contentType)
375375
=> request.AddParameter(new BodyParameter(body, Ensure.NotNull(contentType, nameof(contentType))));
376376

377+
/// <summary>
378+
/// Adds a JSON body parameter to the request from a string
379+
/// </summary>
380+
/// <param name="request">Request instance</param>
381+
/// <param name="forceSerialize">Force serialize the top-level string</param>
382+
/// <param name="contentType">Optional: content type. Default is "application/json"</param>
383+
/// <param name="jsonString">JSON string to be used as a body</param>
384+
/// <returns></returns>
385+
public static RestRequest AddJsonBody(this RestRequest request, string jsonString, bool forceSerialize, ContentType? contentType = null) {
386+
request.RequestFormat = DataFormat.Json;
387+
388+
return !forceSerialize
389+
? request.AddStringBody(jsonString, DataFormat.Json)
390+
: request.AddParameter(new JsonParameter(jsonString, contentType));
391+
}
392+
377393
/// <summary>
378394
/// Adds a JSON body parameter to the request
379395
/// </summary>
@@ -383,7 +399,10 @@ public static RestRequest AddStringBody(this RestRequest request, string body, C
383399
/// <returns></returns>
384400
public static RestRequest AddJsonBody<T>(this RestRequest request, T obj, ContentType? contentType = null) where T : class {
385401
request.RequestFormat = DataFormat.Json;
386-
return obj is string str ? request.AddStringBody(str, DataFormat.Json) : request.AddParameter(new JsonParameter(obj, contentType));
402+
403+
return obj is string str
404+
? request.AddStringBody(str, DataFormat.Json)
405+
: request.AddParameter(new JsonParameter(obj, contentType));
387406
}
388407

389408
/// <summary>
@@ -433,8 +452,8 @@ public static RestRequest AddObject<T>(this RestRequest request, T obj, params s
433452
/// <param name="obj">Object to add as form data</param>
434453
/// <param name="includedProperties">Properties to include, or nothing to include everything. The array will be sorted.</param>
435454
/// <returns></returns>
436-
public static RestRequest AddObjectStatic<T>(this RestRequest request, T obj, params string[] includedProperties) where T : class =>
437-
request.AddParameters(PropertyCache<T>.GetParameters(obj, includedProperties));
455+
public static RestRequest AddObjectStatic<T>(this RestRequest request, T obj, params string[] includedProperties) where T : class
456+
=> request.AddParameters(PropertyCache<T>.GetParameters(obj, includedProperties));
438457

439458
/// <summary>
440459
/// Gets object properties and adds each property as a form data parameter
@@ -448,8 +467,8 @@ public static RestRequest AddObjectStatic<T>(this RestRequest request, T obj, pa
448467
/// <param name="request">Request instance</param>
449468
/// <param name="obj">Object to add as form data</param>
450469
/// <returns></returns>
451-
public static RestRequest AddObjectStatic<T>(this RestRequest request, T obj) where T : class =>
452-
request.AddParameters(PropertyCache<T>.GetParameters(obj));
470+
public static RestRequest AddObjectStatic<T>(this RestRequest request, T obj) where T : class
471+
=> request.AddParameters(PropertyCache<T>.GetParameters(obj));
453472

454473
/// <summary>
455474
/// Adds cookie to the <seealso cref="HttpClient"/> cookie container.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using System.Text.Json;
2+
using RestSharp.Tests.Integrated.Fixtures;
3+
using RestSharp.Tests.Shared.Fixtures;
4+
5+
namespace RestSharp.Tests.Integrated;
6+
7+
public class JsonBodyTests : IClassFixture<RequestBodyFixture> {
8+
readonly SimpleServer _server;
9+
readonly ITestOutputHelper _output;
10+
readonly RestClient _client;
11+
12+
public JsonBodyTests(RequestBodyFixture fixture, ITestOutputHelper output) {
13+
_output = output;
14+
_server = fixture.Server;
15+
_client = new RestClient(_server.Url);
16+
}
17+
18+
[Fact]
19+
public async Task Query_Parameters_With_Json_Body() {
20+
var request = new RestRequest(RequestBodyCapturer.Resource, Method.Put)
21+
.AddJsonBody(new { displayName = "Display Name" })
22+
.AddQueryParameter("key", "value");
23+
24+
await _client.ExecuteAsync(request);
25+
26+
RequestBodyCapturer.CapturedUrl.ToString().Should().Be($"{_server.Url}Capture?key=value");
27+
RequestBodyCapturer.CapturedContentType.Should().Be("application/json; charset=utf-8");
28+
RequestBodyCapturer.CapturedEntityBody.Should().Be("{\"displayName\":\"Display Name\"}");
29+
}
30+
31+
[Fact]
32+
public async Task Add_JSON_body_JSON_string() {
33+
const string payload = "{\"displayName\":\"Display Name\"}";
34+
35+
var request = new RestRequest(RequestBodyCapturer.Resource, Method.Post).AddJsonBody(payload);
36+
37+
await _client.ExecuteAsync(request);
38+
39+
RequestBodyCapturer.CapturedContentType.Should().Be("application/json; charset=utf-8");
40+
RequestBodyCapturer.CapturedEntityBody.Should().Be(payload);
41+
}
42+
43+
[Fact]
44+
public async Task Add_JSON_body_string() {
45+
const string payload = @"
46+
""requestBody"": {
47+
""content"": {
48+
""application/json"": {
49+
""schema"": {
50+
""type"": ""string""
51+
}
52+
}
53+
}
54+
},";
55+
56+
var expected = JsonSerializer.Serialize(payload);
57+
var request = new RestRequest(RequestBodyCapturer.Resource, Method.Post).AddJsonBody(payload, true);
58+
59+
await _client.ExecuteAsync(request);
60+
61+
RequestBodyCapturer.CapturedContentType.Should().Be("application/json; charset=utf-8");
62+
RequestBodyCapturer.CapturedEntityBody.Should().Be(expected);
63+
}
64+
}

test/RestSharp.Tests.Integrated/RequestBodyTests.cs

-17
Original file line numberDiff line numberDiff line change
@@ -106,23 +106,6 @@ public async Task MultipartFormData_Without_File_Creates_A_Valid_RequestBody() {
106106
actual.Should().Contain(expectedBody);
107107
}
108108

109-
[Fact]
110-
public async Task Query_Parameters_With_Json_Body() {
111-
const Method httpMethod = Method.Put;
112-
113-
var client = new RestClient(_server.Url);
114-
115-
var request = new RestRequest(RequestBodyCapturer.Resource, httpMethod)
116-
.AddJsonBody(new { displayName = "Display Name" })
117-
.AddQueryParameter("key", "value");
118-
119-
await client.ExecuteAsync(request);
120-
121-
RequestBodyCapturer.CapturedUrl.ToString().Should().Be($"{_server.Url}Capture?key=value");
122-
RequestBodyCapturer.CapturedContentType.Should().Be("application/json; charset=utf-8");
123-
RequestBodyCapturer.CapturedEntityBody.Should().Be("{\"displayName\":\"Display Name\"}");
124-
}
125-
126109
static void AssertHasNoRequestBody() {
127110
RequestBodyCapturer.CapturedContentType.Should().BeNull();
128111
RequestBodyCapturer.CapturedHasEntityBody.Should().BeFalse();

test/RestSharp.Tests.Integrated/RestSharp.Tests.Integrated.csproj

-3
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,4 @@
2323
<ItemGroup>
2424
<None Update="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
2525
</ItemGroup>
26-
<ItemGroup>
27-
<Folder Include="Models" />
28-
</ItemGroup>
2926
</Project>

0 commit comments

Comments
 (0)