Skip to content

Commit

Permalink
Add AddJsonBody overload to serialise top-level string (#2043)
Browse files Browse the repository at this point in the history
* Added AddJsonBody overload for top-level strings
  • Loading branch information
alexeyzimarev authored Apr 4, 2023
1 parent 2d42a33 commit bf24794
Show file tree
Hide file tree
Showing 9 changed files with 123 additions and 48 deletions.
28 changes: 24 additions & 4 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,17 +339,37 @@ When you call `AddJsonBody`, it does the following for you:
- Sets the content type to `application/json`
- Sets the internal data type of the request body to `DataType.Json`

::: warning
Do not send JSON string or some sort of `JObject` instance to `AddJsonBody`; it won't work! Use `AddStringBody` instead.
:::

Here is the example:

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

It is possible to override the default content type by supplying the `contentType` argument. For example:

```csharp
request.AddJsonBody(param, "text/x-json");
```

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.
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:

```csharp
const string payload = @"
""requestBody"": {
""content"": {
""application/json"": {
""schema"": {
""type"": ""string""
}
}
}
},";
request.AddJsonBody(payload, forceSerialize: true); // the string will be serialized
request.AddJsonBody(payload); // the string will NOT be serialized and will be sent as-is
```

#### AddXmlBody

When you call `AddXmlBody`, it does the following for you:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public class JsonNetSerializer : IRestSerializer, ISerializer, IDeserializer {
public ISerializer Serializer => this;
public IDeserializer Deserializer => this;

public string[] AcceptedContentTypes => RestSharp.ContentType.JsonAccept;
public string[] AcceptedContentTypes => ContentType.JsonAccept;

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

Expand Down
8 changes: 3 additions & 5 deletions src/RestSharp/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
Expand Down Expand Up @@ -144,12 +145,9 @@ internal static IEnumerable<string> GetNameVariants(this string name, CultureInf
yield return name.AddSpaces().ToLower(culture);
}

internal static bool IsEmpty(this string? value) => string.IsNullOrWhiteSpace(value);
internal static bool IsEmpty([NotNullWhen(false)] this string? value) => string.IsNullOrWhiteSpace(value);

internal static bool IsNotEmpty(this string? value) => !string.IsNullOrWhiteSpace(value);

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

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

Expand Down
6 changes: 2 additions & 4 deletions src/RestSharp/Request/BodyExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,15 @@
// limitations under the License.
//

namespace RestSharp;
namespace RestSharp;

using System.Diagnostics.CodeAnalysis;

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

public static bool HasFiles(this RestRequest request) => request.Files.Count > 0;

public static bool IsEmpty([NotNullWhen(false)]this ParametersCollection? parameters) => parameters == null || parameters.Count == 0;
}
12 changes: 4 additions & 8 deletions src/RestSharp/Request/RequestContent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,7 @@ void AddFiles() {

HttpContent Serialize(BodyParameter body) {
return body.DataFormat switch {
DataFormat.None => new StringContent(
body.Value!.ToString()!,
_client.Options.Encoding,
body.ContentType.Value
),
DataFormat.None => new StringContent(body.Value!.ToString()!, _client.Options.Encoding, body.ContentType.Value),
DataFormat.Binary => GetBinary(),
_ => GetSerialized()
};
Expand Down Expand Up @@ -124,19 +120,19 @@ MultipartFormDataContent CreateMultipartFormDataContent() {
void AddBody(bool hasPostParameters) {
if (!_request.TryGetBodyParameter(out var bodyParameter)) return;

var bodyContent = Serialize(bodyParameter!);
var bodyContent = Serialize(bodyParameter);

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

if (name.IsEmpty())
mpContent.Add(bodyContent);
else
mpContent.Add(bodyContent, name!);
mpContent.Add(bodyContent, name);
Content = mpContent;
}
else {
Expand Down
31 changes: 25 additions & 6 deletions src/RestSharp/Request/RestRequestExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ public static RestRequest AddBody(this RestRequest request, object obj, ContentT
DataFormat.Json => request.AddJsonBody(obj, contentType),
DataFormat.Xml => request.AddXmlBody(obj, contentType),
DataFormat.Binary => request.AddParameter(new BodyParameter("", obj, ContentType.Binary)),
_ => request.AddParameter(new BodyParameter("", obj.ToString()!, ContentType.Plain))
_ => request.AddParameter(new BodyParameter("", obj.ToString(), ContentType.Plain))
};
}

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

/// <summary>
/// Adds a JSON body parameter to the request from a string
/// </summary>
/// <param name="request">Request instance</param>
/// <param name="forceSerialize">Force serialize the top-level string</param>
/// <param name="contentType">Optional: content type. Default is "application/json"</param>
/// <param name="jsonString">JSON string to be used as a body</param>
/// <returns></returns>
public static RestRequest AddJsonBody(this RestRequest request, string jsonString, bool forceSerialize, ContentType? contentType = null) {
request.RequestFormat = DataFormat.Json;

return !forceSerialize
? request.AddStringBody(jsonString, DataFormat.Json)
: request.AddParameter(new JsonParameter(jsonString, contentType));
}

/// <summary>
/// Adds a JSON body parameter to the request
/// </summary>
Expand All @@ -383,7 +399,10 @@ public static RestRequest AddStringBody(this RestRequest request, string body, C
/// <returns></returns>
public static RestRequest AddJsonBody<T>(this RestRequest request, T obj, ContentType? contentType = null) where T : class {
request.RequestFormat = DataFormat.Json;
return obj is string str ? request.AddStringBody(str, DataFormat.Json) : request.AddParameter(new JsonParameter(obj, contentType));

return obj is string str
? request.AddStringBody(str, DataFormat.Json)
: request.AddParameter(new JsonParameter(obj, contentType));
}

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

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

/// <summary>
/// Adds cookie to the <seealso cref="HttpClient"/> cookie container.
Expand Down
64 changes: 64 additions & 0 deletions test/RestSharp.Tests.Integrated/JsonBodyTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System.Text.Json;
using RestSharp.Tests.Integrated.Fixtures;
using RestSharp.Tests.Shared.Fixtures;

namespace RestSharp.Tests.Integrated;

public class JsonBodyTests : IClassFixture<RequestBodyFixture> {
readonly SimpleServer _server;
readonly ITestOutputHelper _output;
readonly RestClient _client;

public JsonBodyTests(RequestBodyFixture fixture, ITestOutputHelper output) {
_output = output;
_server = fixture.Server;
_client = new RestClient(_server.Url);
}

[Fact]
public async Task Query_Parameters_With_Json_Body() {
var request = new RestRequest(RequestBodyCapturer.Resource, Method.Put)
.AddJsonBody(new { displayName = "Display Name" })
.AddQueryParameter("key", "value");

await _client.ExecuteAsync(request);

RequestBodyCapturer.CapturedUrl.ToString().Should().Be($"{_server.Url}Capture?key=value");
RequestBodyCapturer.CapturedContentType.Should().Be("application/json; charset=utf-8");
RequestBodyCapturer.CapturedEntityBody.Should().Be("{\"displayName\":\"Display Name\"}");
}

[Fact]
public async Task Add_JSON_body_JSON_string() {
const string payload = "{\"displayName\":\"Display Name\"}";

var request = new RestRequest(RequestBodyCapturer.Resource, Method.Post).AddJsonBody(payload);

await _client.ExecuteAsync(request);

RequestBodyCapturer.CapturedContentType.Should().Be("application/json; charset=utf-8");
RequestBodyCapturer.CapturedEntityBody.Should().Be(payload);
}

[Fact]
public async Task Add_JSON_body_string() {
const string payload = @"
""requestBody"": {
""content"": {
""application/json"": {
""schema"": {
""type"": ""string""
}
}
}
},";

var expected = JsonSerializer.Serialize(payload);
var request = new RestRequest(RequestBodyCapturer.Resource, Method.Post).AddJsonBody(payload, true);

await _client.ExecuteAsync(request);

RequestBodyCapturer.CapturedContentType.Should().Be("application/json; charset=utf-8");
RequestBodyCapturer.CapturedEntityBody.Should().Be(expected);
}
}
17 changes: 0 additions & 17 deletions test/RestSharp.Tests.Integrated/RequestBodyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,23 +106,6 @@ public async Task MultipartFormData_Without_File_Creates_A_Valid_RequestBody() {
actual.Should().Contain(expectedBody);
}

[Fact]
public async Task Query_Parameters_With_Json_Body() {
const Method httpMethod = Method.Put;

var client = new RestClient(_server.Url);

var request = new RestRequest(RequestBodyCapturer.Resource, httpMethod)
.AddJsonBody(new { displayName = "Display Name" })
.AddQueryParameter("key", "value");

await client.ExecuteAsync(request);

RequestBodyCapturer.CapturedUrl.ToString().Should().Be($"{_server.Url}Capture?key=value");
RequestBodyCapturer.CapturedContentType.Should().Be("application/json; charset=utf-8");
RequestBodyCapturer.CapturedEntityBody.Should().Be("{\"displayName\":\"Display Name\"}");
}

static void AssertHasNoRequestBody() {
RequestBodyCapturer.CapturedContentType.Should().BeNull();
RequestBodyCapturer.CapturedHasEntityBody.Should().BeFalse();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,4 @@
<ItemGroup>
<None Update="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<Folder Include="Models" />
</ItemGroup>
</Project>

0 comments on commit bf24794

Please sign in to comment.