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 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
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,13 @@ 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, bool validate = false) { throw null; }
[System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation.")]
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, bool validate = false) { 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
106 changes: 105 additions & 1 deletion sdk/core/System.ClientModel/src/Message/BinaryContent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
using System.Buffers;
using System.ClientModel.Internal;
using System.ClientModel.Primitives;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using System.Threading;
using System.Threading.Tasks;

Expand All @@ -17,6 +20,13 @@ namespace System.ClientModel;
public abstract class BinaryContent : IDisposable
{
private static readonly ModelReaderWriterOptions ModelWriteWireOptions = new ModelReaderWriterOptions("W");
private const string JsonSerializerRequiresDynamicCode = "JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation.";
private const string JsonSerializerRequiresUnreferencedCode = "JSON serialization and deserialization might require types that cannot be statically analyzed.";

/// <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
Expand Down Expand Up @@ -65,6 +75,91 @@ 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
[RequiresDynamicCode(JsonSerializerRequiresDynamicCode)]
[RequiresUnreferencedCode(JsonSerializerRequiresUnreferencedCode)]
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>
/// <param name="validate">Whether to validate that the string contains valid JSON.
/// If true, the method will validate the JSON format and throw an exception if invalid.
/// Defaults to false for backwards compatibility.</param>
/// <returns>An instance of <see cref="BinaryContent"/> that contains the
/// provided JSON string.</returns>
/// <exception cref="ArgumentException">Thrown when <paramref name="validate"/> is true and
/// <paramref name="jsonString"/> is not valid JSON.</exception>
public static BinaryContent CreateJson(string jsonString, bool validate = false)
{
Argument.AssertNotNull(jsonString, nameof(jsonString));

if (validate)
{
ValidateJsonString(jsonString);
}

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

private static void ValidateJsonString(string jsonString)
{
try
{
var reader = new Utf8JsonReader(System.Text.Encoding.UTF8.GetBytes(jsonString));

// Skip through the entire JSON document to validate it's well-formed
while (reader.Read())
{
// The Read() method will throw JsonException if the JSON is malformed
}
}
catch (JsonException ex)
{
throw new ArgumentException($"The provided string is not valid JSON: {ex.Message}", nameof(jsonString), ex);
}
}

/// <summary>
/// Attempts to compute the length of the underlying body content, if available.
/// </summary>
Expand Down Expand Up @@ -96,9 +191,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 +236,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
Loading