Skip to content

Add CreateJson overloads to BinaryContent for simplified JSON serialization #50921

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

Merged
merged 11 commits into from
Jul 1, 2025
Merged
Show file tree
Hide file tree
Changes from 9 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
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ protected AuthenticationTokenProvider() { }
public abstract partial class BinaryContent : System.IDisposable
{
protected BinaryContent() { }
public string? MediaType { get { throw null; } protected set { } }
public static System.ClientModel.BinaryContent Create(System.BinaryData value) { throw null; }
public static System.ClientModel.BinaryContent Create(System.IO.Stream stream) { throw null; }
public static System.ClientModel.BinaryContent CreateJson(string jsonString) { throw null; }
public static System.ClientModel.BinaryContent CreateJson<T>(T jsonSerializable, System.Text.Json.JsonSerializerOptions? options = null) { throw null; }
public static System.ClientModel.BinaryContent CreateJson<T>(T jsonSerializable, System.Text.Json.Serialization.Metadata.JsonTypeInfo<T> jsonTypeInfo) { throw null; }
public static System.ClientModel.BinaryContent Create<T>(T model, System.ClientModel.Primitives.ModelReaderWriterOptions? options = null) where T : System.ClientModel.Primitives.IPersistableModel<T> { throw null; }
public abstract void Dispose();
public abstract bool TryComputeLength(out long length);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ protected AuthenticationTokenProvider() { }
public abstract partial class BinaryContent : System.IDisposable
{
protected BinaryContent() { }
public string? MediaType { get { throw null; } protected set { } }
public static System.ClientModel.BinaryContent Create(System.BinaryData value) { throw null; }
public static System.ClientModel.BinaryContent Create(System.IO.Stream stream) { throw null; }
public static System.ClientModel.BinaryContent CreateJson(string jsonString) { throw null; }
public static System.ClientModel.BinaryContent CreateJson<T>(T jsonSerializable, System.Text.Json.JsonSerializerOptions? options = null) { throw null; }
public static System.ClientModel.BinaryContent CreateJson<T>(T jsonSerializable, System.Text.Json.Serialization.Metadata.JsonTypeInfo<T> jsonTypeInfo) { throw null; }
public static System.ClientModel.BinaryContent Create<T>(T model, System.ClientModel.Primitives.ModelReaderWriterOptions? options = null) where T : System.ClientModel.Primitives.IPersistableModel<T> { throw null; }
public abstract void Dispose();
public abstract bool TryComputeLength(out long length);
Expand Down
73 changes: 72 additions & 1 deletion sdk/core/System.ClientModel/src/Message/BinaryContent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using System.ClientModel.Internal;
using System.ClientModel.Primitives;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using System.Threading;
using System.Threading.Tasks;

Expand All @@ -18,6 +20,11 @@ public abstract class BinaryContent : IDisposable
{
private static readonly ModelReaderWriterOptions ModelWriteWireOptions = new ModelReaderWriterOptions("W");

/// <summary>
/// Gets the media type of the content.
/// </summary>
public string? MediaType { get; protected set; }

/// <summary>
/// Creates an instance of <see cref="BinaryContent"/> that contains the
/// bytes held in the provided <see cref="BinaryData"/> instance.
Expand Down Expand Up @@ -65,6 +72,61 @@ public static BinaryContent Create(Stream stream)
return new StreamBinaryContent(stream);
}

/// <summary>
/// Creates an instance of <see cref="BinaryContent"/> that contains the
/// JSON representation of the provided object.
/// </summary>
/// <typeparam name="T">The type of the object to serialize.</typeparam>
/// <param name="jsonSerializable">The object to serialize to JSON.</param>
/// <param name="options">The <see cref="JsonSerializerOptions"/> to use for serialization.
/// If not provided, the default options will be used.</param>
/// <returns>An instance of <see cref="BinaryContent"/> that contains the
/// JSON representation of the provided object.</returns>
#pragma warning disable AZC0014 // Avoid using banned types in public API
public static BinaryContent CreateJson<T>(T jsonSerializable, JsonSerializerOptions? options = default)
#pragma warning restore AZC0014 // Avoid using banned types in public API
{
Argument.AssertNotNull(jsonSerializable, nameof(jsonSerializable));

BinaryData data = BinaryData.FromObjectAsJson(jsonSerializable, options);
return new BinaryDataBinaryContent(data.ToMemory(), "application/json");
}

/// <summary>
/// Creates an instance of <see cref="BinaryContent"/> that contains the
/// JSON representation of the provided object using the specified JSON type information.
/// </summary>
/// <typeparam name="T">The type of the object to serialize.</typeparam>
/// <param name="jsonSerializable">The object to serialize to JSON.</param>
/// <param name="jsonTypeInfo">The <see cref="JsonTypeInfo{T}"/> to use for serialization.</param>
/// <returns>An instance of <see cref="BinaryContent"/> that contains the
/// JSON representation of the provided object.</returns>
#pragma warning disable AZC0014 // Avoid using banned types in public API
public static BinaryContent CreateJson<T>(T jsonSerializable, JsonTypeInfo<T> jsonTypeInfo)
#pragma warning restore AZC0014 // Avoid using banned types in public API
{
Argument.AssertNotNull(jsonSerializable, nameof(jsonSerializable));
Argument.AssertNotNull(jsonTypeInfo, nameof(jsonTypeInfo));

BinaryData data = BinaryData.FromObjectAsJson(jsonSerializable, jsonTypeInfo);
return new BinaryDataBinaryContent(data.ToMemory(), "application/json");
}

/// <summary>
/// Creates an instance of <see cref="BinaryContent"/> that contains the
/// provided JSON string.
/// </summary>
/// <param name="jsonString">The JSON string to be used as the content.</param>
/// <returns>An instance of <see cref="BinaryContent"/> that contains the
/// provided JSON string.</returns>
public static BinaryContent CreateJson(string jsonString)
{
Argument.AssertNotNull(jsonString, nameof(jsonString));

BinaryData data = BinaryData.FromString(jsonString);
return new BinaryDataBinaryContent(data.ToMemory(), "application/json");
}

/// <summary>
/// Attempts to compute the length of the underlying body content, if available.
/// </summary>
Expand Down Expand Up @@ -96,9 +158,10 @@ private sealed class BinaryDataBinaryContent : BinaryContent
{
private readonly ReadOnlyMemory<byte> _bytes;

public BinaryDataBinaryContent(ReadOnlyMemory<byte> bytes)
public BinaryDataBinaryContent(ReadOnlyMemory<byte> bytes, string? mediaType = null)
{
_bytes = bytes;
MediaType = mediaType;
}

public override bool TryComputeLength(out long length)
Expand Down Expand Up @@ -140,6 +203,14 @@ public ModelBinaryContent(T model, ModelReaderWriterOptions options)
{
_model = model;
_options = options;

// Set MediaType to JSON if the model will be serialized as JSON
// This checks if JSON format is requested, either explicitly ("J") or
// via wire format ("W") where the model returns "J" as its preferred format
if (options.Format == "J" || (options.Format == "W" && model.GetFormatFromOptions(options) == "J"))
{
MediaType = "application/json";
}
}

private UnsafeBufferSequence.Reader SequenceReader
Expand Down
104 changes: 104 additions & 0 deletions sdk/core/System.ClientModel/tests/Message/BinaryContentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.ClientModel.Primitives;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using System.Threading;
using System.Threading.Tasks;
using Azure.Core.TestFramework;
Expand All @@ -26,6 +27,7 @@ public void CanGetLengthFromBinaryDataBinaryContent()
BinaryData data = BinaryData.FromString(value);
using BinaryContent content = BinaryContent.Create(data);

Assert.IsNull(content.MediaType);
Assert.IsTrue(content.TryComputeLength(out long length));
Assert.AreEqual(value.Length, length);
}
Expand All @@ -50,6 +52,7 @@ public void CanGetLengthFromModelBinaryContent()
MockPersistableModel model = new MockPersistableModel(404, "abcde");
using BinaryContent content = BinaryContent.Create(model);

Assert.AreEqual("application/json", content.MediaType);
Assert.IsTrue(content.TryComputeLength(out long length));
Assert.AreEqual(model.SerializedValue.Length, length);
}
Expand All @@ -60,6 +63,8 @@ public async Task CanWriteToStreamFromModelBinaryContent()
MockPersistableModel model = new MockPersistableModel(404, "abcde");
using BinaryContent content = BinaryContent.Create(model);

Assert.AreEqual("application/json", content.MediaType);

MemoryStream stream = new MemoryStream();
await content.WriteToSyncOrAsync(stream, CancellationToken.None, IsAsync);

Expand All @@ -75,6 +80,7 @@ public void CanGetLengthFromJsonModelBinaryContent()
MockJsonModel model = new MockJsonModel(404, "abcde");
using BinaryContent content = BinaryContent.Create(model, ModelReaderWriterOptions.Json);

Assert.AreEqual("application/json", content.MediaType);
Assert.IsTrue(content.TryComputeLength(out long length));
Assert.AreEqual(model.Utf8BytesValue.Length, length);
}
Expand All @@ -85,6 +91,8 @@ public async Task CanWriteToStreamFromJsonModelBinaryContent()
MockJsonModel model = new MockJsonModel(404, "abcde");
using BinaryContent content = BinaryContent.Create(model, ModelReaderWriterOptions.Json);

Assert.AreEqual("application/json", content.MediaType);

MemoryStream contentStream = new MemoryStream();
await content.WriteToSyncOrAsync(contentStream, CancellationToken.None, IsAsync);

Expand Down Expand Up @@ -156,4 +164,100 @@ public void StreamBinaryContentMustBeSeekable()

Assert.Throws<ArgumentException>(() => { BinaryContent.Create(stream); });
}

[Test]
public async Task CanCreateAndWriteJsonBinaryContentFromObject()
{
var testObject = new { Name = "test", Value = 42 };
using BinaryContent content = BinaryContent.CreateJson(testObject);

Assert.AreEqual("application/json", content.MediaType);
Assert.IsTrue(content.TryComputeLength(out long length));
Assert.Greater(length, 0);

MemoryStream stream = new MemoryStream();
await content.WriteToSyncOrAsync(stream, CancellationToken.None, IsAsync);

string json = System.Text.Encoding.UTF8.GetString(stream.ToArray());
Assert.IsTrue(json.Contains("test"));
Assert.IsTrue(json.Contains("42"));
}

[Test]
public async Task CanCreateAndWriteJsonBinaryContentWithOptions()
{
var testObject = new { Name = "TEST", Value = 42 };
var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };

using BinaryContent content = BinaryContent.CreateJson(testObject, options);

Assert.AreEqual("application/json", content.MediaType);
Assert.IsTrue(content.TryComputeLength(out long length));
Assert.Greater(length, 0);

MemoryStream stream = new MemoryStream();
await content.WriteToSyncOrAsync(stream, CancellationToken.None, IsAsync);

string json = System.Text.Encoding.UTF8.GetString(stream.ToArray());
// With camelCase naming policy, "Name" should become "name"
Assert.IsTrue(json.Contains("name"));
Assert.IsTrue(json.Contains("value"));
}

[Test]
public void JsonBinaryContentMatchesBinaryDataFromObjectAsJson()
{
var testObject = new { Name = "test", Value = 42 };

// Create using the new CreateJson method
using BinaryContent content = BinaryContent.CreateJson(testObject);

// Create using the existing pattern
BinaryData binaryData = BinaryData.FromObjectAsJson(testObject);
using BinaryContent expectedContent = BinaryContent.Create(binaryData);

// They should have the same length
Assert.IsTrue(content.TryComputeLength(out long contentLength));
Assert.IsTrue(expectedContent.TryComputeLength(out long expectedLength));
Assert.AreEqual(expectedLength, contentLength);
}

[Test]
public async Task CanCreateAndWriteJsonBinaryContentFromString()
{
string jsonString = """{"name":"test","value":42}""";
using BinaryContent content = BinaryContent.CreateJson(jsonString);

Assert.AreEqual("application/json", content.MediaType);
Assert.IsTrue(content.TryComputeLength(out long length));
Assert.AreEqual(jsonString.Length, length);

MemoryStream stream = new MemoryStream();
await content.WriteToSyncOrAsync(stream, CancellationToken.None, IsAsync);

string result = System.Text.Encoding.UTF8.GetString(stream.ToArray());
Assert.AreEqual(jsonString, result);
}

[Test]
public void JsonStringBinaryContentMatchesBinaryDataFromString()
{
string jsonString = """{"name":"test","value":42}""";

// Create using the new CreateJson string method
using BinaryContent content = BinaryContent.CreateJson(jsonString);

// Create using the existing pattern
BinaryData binaryData = BinaryData.FromString(jsonString);
using BinaryContent expectedContent = BinaryContent.Create(binaryData);

// They should have the same length
Assert.IsTrue(content.TryComputeLength(out long contentLength));
Assert.IsTrue(expectedContent.TryComputeLength(out long expectedLength));
Assert.AreEqual(expectedLength, contentLength);

// Content should have JSON media type, while regular Create should not
Assert.AreEqual("application/json", content.MediaType);
Assert.IsNull(expectedContent.MediaType);
}
}
Loading