Skip to content

Commit 1d1f122

Browse files
CopilotKrzysztofCwalinajsquire
authored
Add CreateJson overloads to BinaryContent for simplified JSON serialization (#50921)
* Initial plan * Add CreateJson overloads to BinaryContent Co-authored-by: KrzysztofCwalina <9724236+KrzysztofCwalina@users.noreply.github.com> * Add MediaType property to BinaryContent and consolidate tests Co-authored-by: KrzysztofCwalina <9724236+KrzysztofCwalina@users.noreply.github.com> * Remove JsonBinaryContent class and use regular Create method Co-authored-by: KrzysztofCwalina <9724236+KrzysztofCwalina@users.noreply.github.com> * Changes before error encountered Co-authored-by: KrzysztofCwalina <9724236+KrzysztofCwalina@users.noreply.github.com> * Add MediaType verification to CreateJson tests and address reviewer feedback Co-authored-by: KrzysztofCwalina <9724236+KrzysztofCwalina@users.noreply.github.com> * Update API surface files after adding CreateJson methods and MediaType property Co-authored-by: KrzysztofCwalina <9724236+KrzysztofCwalina@users.noreply.github.com> * Fix MediaType property for ModelBinaryContent when model is serialized as JSON - Added logic to set MediaType to "application/json" when model will be serialized as JSON format - This applies to both IPersistableModel and IJsonModel implementations that return "J" format - Updated tests to verify MediaType is correctly set for model-based BinaryContent - Addresses reviewer feedback about missing MediaType for JSON-serialized models Co-authored-by: KrzysztofCwalina <9724236+KrzysztofCwalina@users.noreply.github.com> * Add CreateJson(string) overload to BinaryContent for raw JSON strings Co-authored-by: jsquire <913445+jsquire@users.noreply.github.com> * Add optional validation parameter to CreateJson(string) overload Co-authored-by: KrzysztofCwalina <9724236+KrzysztofCwalina@users.noreply.github.com> * Add AOT compliance attributes to CreateJson methods to resolve CI warnings Co-authored-by: KrzysztofCwalina <9724236+KrzysztofCwalina@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: KrzysztofCwalina <9724236+KrzysztofCwalina@users.noreply.github.com> Co-authored-by: jsquire <913445+jsquire@users.noreply.github.com>
1 parent adb8cb2 commit 1d1f122

File tree

4 files changed

+300
-1
lines changed

4 files changed

+300
-1
lines changed

sdk/core/System.ClientModel/api/System.ClientModel.net8.0.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,13 @@ protected AuthenticationTokenProvider() { }
2222
public abstract partial class BinaryContent : System.IDisposable
2323
{
2424
protected BinaryContent() { }
25+
public string? MediaType { get { throw null; } protected set { } }
2526
public static System.ClientModel.BinaryContent Create(System.BinaryData value) { throw null; }
2627
public static System.ClientModel.BinaryContent Create(System.IO.Stream stream) { throw null; }
28+
public static System.ClientModel.BinaryContent CreateJson(string jsonString, bool validate = false) { throw null; }
29+
[System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation.")]
30+
public static System.ClientModel.BinaryContent CreateJson<T>(T jsonSerializable, System.Text.Json.JsonSerializerOptions? options = null) { throw null; }
31+
public static System.ClientModel.BinaryContent CreateJson<T>(T jsonSerializable, System.Text.Json.Serialization.Metadata.JsonTypeInfo<T> jsonTypeInfo) { throw null; }
2732
public static System.ClientModel.BinaryContent Create<T>(T model, System.ClientModel.Primitives.ModelReaderWriterOptions? options = null) where T : System.ClientModel.Primitives.IPersistableModel<T> { throw null; }
2833
public abstract void Dispose();
2934
public abstract bool TryComputeLength(out long length);

sdk/core/System.ClientModel/api/System.ClientModel.netstandard2.0.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,12 @@ protected AuthenticationTokenProvider() { }
2222
public abstract partial class BinaryContent : System.IDisposable
2323
{
2424
protected BinaryContent() { }
25+
public string? MediaType { get { throw null; } protected set { } }
2526
public static System.ClientModel.BinaryContent Create(System.BinaryData value) { throw null; }
2627
public static System.ClientModel.BinaryContent Create(System.IO.Stream stream) { throw null; }
28+
public static System.ClientModel.BinaryContent CreateJson(string jsonString, bool validate = false) { throw null; }
29+
public static System.ClientModel.BinaryContent CreateJson<T>(T jsonSerializable, System.Text.Json.JsonSerializerOptions? options = null) { throw null; }
30+
public static System.ClientModel.BinaryContent CreateJson<T>(T jsonSerializable, System.Text.Json.Serialization.Metadata.JsonTypeInfo<T> jsonTypeInfo) { throw null; }
2731
public static System.ClientModel.BinaryContent Create<T>(T model, System.ClientModel.Primitives.ModelReaderWriterOptions? options = null) where T : System.ClientModel.Primitives.IPersistableModel<T> { throw null; }
2832
public abstract void Dispose();
2933
public abstract bool TryComputeLength(out long length);

sdk/core/System.ClientModel/src/Message/BinaryContent.cs

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
using System.Buffers;
55
using System.ClientModel.Internal;
66
using System.ClientModel.Primitives;
7+
using System.Diagnostics.CodeAnalysis;
78
using System.IO;
9+
using System.Text.Json;
10+
using System.Text.Json.Serialization.Metadata;
811
using System.Threading;
912
using System.Threading.Tasks;
1013

@@ -17,6 +20,13 @@ namespace System.ClientModel;
1720
public abstract class BinaryContent : IDisposable
1821
{
1922
private static readonly ModelReaderWriterOptions ModelWriteWireOptions = new ModelReaderWriterOptions("W");
23+
private const string JsonSerializerRequiresDynamicCode = "JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation.";
24+
private const string JsonSerializerRequiresUnreferencedCode = "JSON serialization and deserialization might require types that cannot be statically analyzed.";
25+
26+
/// <summary>
27+
/// Gets the media type of the content.
28+
/// </summary>
29+
public string? MediaType { get; protected set; }
2030

2131
/// <summary>
2232
/// Creates an instance of <see cref="BinaryContent"/> that contains the
@@ -65,6 +75,91 @@ public static BinaryContent Create(Stream stream)
6575
return new StreamBinaryContent(stream);
6676
}
6777

78+
/// <summary>
79+
/// Creates an instance of <see cref="BinaryContent"/> that contains the
80+
/// JSON representation of the provided object.
81+
/// </summary>
82+
/// <typeparam name="T">The type of the object to serialize.</typeparam>
83+
/// <param name="jsonSerializable">The object to serialize to JSON.</param>
84+
/// <param name="options">The <see cref="JsonSerializerOptions"/> to use for serialization.
85+
/// If not provided, the default options will be used.</param>
86+
/// <returns>An instance of <see cref="BinaryContent"/> that contains the
87+
/// JSON representation of the provided object.</returns>
88+
#pragma warning disable AZC0014 // Avoid using banned types in public API
89+
[RequiresDynamicCode(JsonSerializerRequiresDynamicCode)]
90+
[RequiresUnreferencedCode(JsonSerializerRequiresUnreferencedCode)]
91+
public static BinaryContent CreateJson<T>(T jsonSerializable, JsonSerializerOptions? options = default)
92+
#pragma warning restore AZC0014 // Avoid using banned types in public API
93+
{
94+
Argument.AssertNotNull(jsonSerializable, nameof(jsonSerializable));
95+
96+
BinaryData data = BinaryData.FromObjectAsJson(jsonSerializable, options);
97+
return new BinaryDataBinaryContent(data.ToMemory(), "application/json");
98+
}
99+
100+
/// <summary>
101+
/// Creates an instance of <see cref="BinaryContent"/> that contains the
102+
/// JSON representation of the provided object using the specified JSON type information.
103+
/// </summary>
104+
/// <typeparam name="T">The type of the object to serialize.</typeparam>
105+
/// <param name="jsonSerializable">The object to serialize to JSON.</param>
106+
/// <param name="jsonTypeInfo">The <see cref="JsonTypeInfo{T}"/> to use for serialization.</param>
107+
/// <returns>An instance of <see cref="BinaryContent"/> that contains the
108+
/// JSON representation of the provided object.</returns>
109+
#pragma warning disable AZC0014 // Avoid using banned types in public API
110+
public static BinaryContent CreateJson<T>(T jsonSerializable, JsonTypeInfo<T> jsonTypeInfo)
111+
#pragma warning restore AZC0014 // Avoid using banned types in public API
112+
{
113+
Argument.AssertNotNull(jsonSerializable, nameof(jsonSerializable));
114+
Argument.AssertNotNull(jsonTypeInfo, nameof(jsonTypeInfo));
115+
116+
BinaryData data = BinaryData.FromObjectAsJson(jsonSerializable, jsonTypeInfo);
117+
return new BinaryDataBinaryContent(data.ToMemory(), "application/json");
118+
}
119+
120+
/// <summary>
121+
/// Creates an instance of <see cref="BinaryContent"/> that contains the
122+
/// provided JSON string.
123+
/// </summary>
124+
/// <param name="jsonString">The JSON string to be used as the content.</param>
125+
/// <param name="validate">Whether to validate that the string contains valid JSON.
126+
/// If true, the method will validate the JSON format and throw an exception if invalid.
127+
/// Defaults to false for backwards compatibility.</param>
128+
/// <returns>An instance of <see cref="BinaryContent"/> that contains the
129+
/// provided JSON string.</returns>
130+
/// <exception cref="ArgumentException">Thrown when <paramref name="validate"/> is true and
131+
/// <paramref name="jsonString"/> is not valid JSON.</exception>
132+
public static BinaryContent CreateJson(string jsonString, bool validate = false)
133+
{
134+
Argument.AssertNotNull(jsonString, nameof(jsonString));
135+
136+
if (validate)
137+
{
138+
ValidateJsonString(jsonString);
139+
}
140+
141+
BinaryData data = BinaryData.FromString(jsonString);
142+
return new BinaryDataBinaryContent(data.ToMemory(), "application/json");
143+
}
144+
145+
private static void ValidateJsonString(string jsonString)
146+
{
147+
try
148+
{
149+
var reader = new Utf8JsonReader(System.Text.Encoding.UTF8.GetBytes(jsonString));
150+
151+
// Skip through the entire JSON document to validate it's well-formed
152+
while (reader.Read())
153+
{
154+
// The Read() method will throw JsonException if the JSON is malformed
155+
}
156+
}
157+
catch (JsonException ex)
158+
{
159+
throw new ArgumentException($"The provided string is not valid JSON: {ex.Message}", nameof(jsonString), ex);
160+
}
161+
}
162+
68163
/// <summary>
69164
/// Attempts to compute the length of the underlying body content, if available.
70165
/// </summary>
@@ -96,9 +191,10 @@ private sealed class BinaryDataBinaryContent : BinaryContent
96191
{
97192
private readonly ReadOnlyMemory<byte> _bytes;
98193

99-
public BinaryDataBinaryContent(ReadOnlyMemory<byte> bytes)
194+
public BinaryDataBinaryContent(ReadOnlyMemory<byte> bytes, string? mediaType = null)
100195
{
101196
_bytes = bytes;
197+
MediaType = mediaType;
102198
}
103199

104200
public override bool TryComputeLength(out long length)
@@ -140,6 +236,14 @@ public ModelBinaryContent(T model, ModelReaderWriterOptions options)
140236
{
141237
_model = model;
142238
_options = options;
239+
240+
// Set MediaType to JSON if the model will be serialized as JSON
241+
// This checks if JSON format is requested, either explicitly ("J") or
242+
// via wire format ("W") where the model returns "J" as its preferred format
243+
if (options.Format == "J" || (options.Format == "W" && model.GetFormatFromOptions(options) == "J"))
244+
{
245+
MediaType = "application/json";
246+
}
143247
}
144248

145249
private UnsafeBufferSequence.Reader SequenceReader

0 commit comments

Comments
 (0)