From bf247946ea4fdf70cc6fc2a454041854b146eb02 Mon Sep 17 00:00:00 2001 From: Alexey Zimarev Date: Tue, 4 Apr 2023 18:33:13 +0200 Subject: [PATCH] Add `AddJsonBody` overload to serialise top-level string (#2043) * Added AddJsonBody overload for top-level strings --- docs/usage.md | 28 ++++++-- .../JsonNetSerializer.cs | 2 +- src/RestSharp/Extensions/StringExtensions.cs | 8 +-- src/RestSharp/Request/BodyExtensions.cs | 6 +- src/RestSharp/Request/RequestContent.cs | 12 ++-- .../Request/RestRequestExtensions.cs | 31 +++++++-- .../JsonBodyTests.cs | 64 +++++++++++++++++++ .../RequestBodyTests.cs | 17 ----- .../RestSharp.Tests.Integrated.csproj | 3 - 9 files changed, 123 insertions(+), 48 deletions(-) create mode 100644 test/RestSharp.Tests.Integrated/JsonBodyTests.cs diff --git a/docs/usage.md b/docs/usage.md index 5b76b15e0..8e41cc314 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -339,10 +339,6 @@ 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 @@ -350,6 +346,30 @@ 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: diff --git a/src/RestSharp.Serializers.NewtonsoftJson/JsonNetSerializer.cs b/src/RestSharp.Serializers.NewtonsoftJson/JsonNetSerializer.cs index 61a08116a..94e60b40b 100644 --- a/src/RestSharp.Serializers.NewtonsoftJson/JsonNetSerializer.cs +++ b/src/RestSharp.Serializers.NewtonsoftJson/JsonNetSerializer.cs @@ -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; diff --git a/src/RestSharp/Extensions/StringExtensions.cs b/src/RestSharp/Extensions/StringExtensions.cs index cddbd3670..e4ddb718f 100644 --- a/src/RestSharp/Extensions/StringExtensions.cs +++ b/src/RestSharp/Extensions/StringExtensions.cs @@ -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; @@ -144,12 +145,9 @@ internal static string ToCamelCase(this string lowercaseAndUnderscoredWord, Cult 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(this IEnumerable collection, string separator, Func getString) - => JoinToString(collection.Select(getString), separator); + internal static bool IsNotEmpty([NotNullWhen(true)] this string? value) => !string.IsNullOrWhiteSpace(value); internal static string JoinToString(this IEnumerable strings, string separator) => string.Join(separator, strings); diff --git a/src/RestSharp/Request/BodyExtensions.cs b/src/RestSharp/Request/BodyExtensions.cs index 55b5c4de9..1c6a592e9 100644 --- a/src/RestSharp/Request/BodyExtensions.cs +++ b/src/RestSharp/Request/BodyExtensions.cs @@ -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; } diff --git a/src/RestSharp/Request/RequestContent.cs b/src/RestSharp/Request/RequestContent.cs index 308d1ba86..3ba769012 100644 --- a/src/RestSharp/Request/RequestContent.cs +++ b/src/RestSharp/Request/RequestContent.cs @@ -72,11 +72,7 @@ class RequestContent : IDisposable { 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() }; @@ -124,19 +120,19 @@ class RequestContent : IDisposable { 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 { diff --git a/src/RestSharp/Request/RestRequestExtensions.cs b/src/RestSharp/Request/RestRequestExtensions.cs index 4ea84ba64..2e429d91d 100644 --- a/src/RestSharp/Request/RestRequestExtensions.cs +++ b/src/RestSharp/Request/RestRequestExtensions.cs @@ -338,7 +338,7 @@ public static RestRequest AddOrUpdateParameter(this RestRequest request, Paramet 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)) }; } @@ -374,6 +374,22 @@ public static RestRequest AddOrUpdateParameter(this RestRequest request, Paramet public static RestRequest AddStringBody(this RestRequest request, string body, ContentType contentType) => request.AddParameter(new BodyParameter(body, Ensure.NotNull(contentType, nameof(contentType)))); + /// + /// Adds a JSON body parameter to the request from a string + /// + /// Request instance + /// Force serialize the top-level string + /// Optional: content type. Default is "application/json" + /// JSON string to be used as a body + /// + 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)); + } + /// /// Adds a JSON body parameter to the request /// @@ -383,7 +399,10 @@ public static RestRequest AddStringBody(this RestRequest request, string body, C /// public static RestRequest AddJsonBody(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)); } /// @@ -433,8 +452,8 @@ public static RestRequest AddXmlBody(this RestRequest request, T obj, Content /// Object to add as form data /// Properties to include, or nothing to include everything. The array will be sorted. /// - public static RestRequest AddObjectStatic(this RestRequest request, T obj, params string[] includedProperties) where T : class => - request.AddParameters(PropertyCache.GetParameters(obj, includedProperties)); + public static RestRequest AddObjectStatic(this RestRequest request, T obj, params string[] includedProperties) where T : class + => request.AddParameters(PropertyCache.GetParameters(obj, includedProperties)); /// /// Gets object properties and adds each property as a form data parameter @@ -448,8 +467,8 @@ public static RestRequest AddXmlBody(this RestRequest request, T obj, Content /// Request instance /// Object to add as form data /// - public static RestRequest AddObjectStatic(this RestRequest request, T obj) where T : class => - request.AddParameters(PropertyCache.GetParameters(obj)); + public static RestRequest AddObjectStatic(this RestRequest request, T obj) where T : class + => request.AddParameters(PropertyCache.GetParameters(obj)); /// /// Adds cookie to the cookie container. diff --git a/test/RestSharp.Tests.Integrated/JsonBodyTests.cs b/test/RestSharp.Tests.Integrated/JsonBodyTests.cs new file mode 100644 index 000000000..ab09ba37d --- /dev/null +++ b/test/RestSharp.Tests.Integrated/JsonBodyTests.cs @@ -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 { + 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); + } +} diff --git a/test/RestSharp.Tests.Integrated/RequestBodyTests.cs b/test/RestSharp.Tests.Integrated/RequestBodyTests.cs index 41f9181f1..a7c5df2c0 100644 --- a/test/RestSharp.Tests.Integrated/RequestBodyTests.cs +++ b/test/RestSharp.Tests.Integrated/RequestBodyTests.cs @@ -106,23 +106,6 @@ public class RequestBodyTests : IClassFixture { 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(); diff --git a/test/RestSharp.Tests.Integrated/RestSharp.Tests.Integrated.csproj b/test/RestSharp.Tests.Integrated/RestSharp.Tests.Integrated.csproj index 0fe9a8e69..f084e1ecb 100644 --- a/test/RestSharp.Tests.Integrated/RestSharp.Tests.Integrated.csproj +++ b/test/RestSharp.Tests.Integrated/RestSharp.Tests.Integrated.csproj @@ -23,7 +23,4 @@ - - - \ No newline at end of file