Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add AddJsonBody overload to serialise top-level string #2043

Merged
merged 2 commits into from
Apr 4, 2023
Merged
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
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>