using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
using System.Text.Json.Serialization;
public static partial class JsonSerializerExtensions
public static void Serialize<TValue>(TextWriter textWriter, TValue value, JsonSerializerOptions? options = default)
throw new ArgumentNullException(nameof(textWriter));
using (var stream = textWriter.AsWrappedWriteOnlyStream(Encoding.UTF8, true))
JsonSerializer.Serialize(stream, value, options);
public static async Task SerializeAsync<TValue>(TextWriter textWriter, TValue value, JsonSerializerOptions? options = default, CancellationToken cancellationToken = default)
throw new ArgumentNullException(nameof(textWriter));
await using (var stream = textWriter.AsWrappedWriteOnlyStream(Encoding.UTF8, true))
await JsonSerializer.SerializeAsync(stream, value, options);
public static partial class TextExtensions
public static Encoding PlatformCompatibleUnicode { get; } = BitConverter.IsLittleEndian ? Encoding.Unicode : Encoding.BigEndianUnicode;
public static bool IsPlatformCompatibleUnicode(this Encoding encoding) => BitConverter.IsLittleEndian ? encoding.CodePage == 1200 : encoding.CodePage == 1201;
public static Stream AsWrappedWriteOnlyStream(this TextWriter textWriter, Encoding outerEncoding, bool leaveOpen = false)
if (textWriter == null || outerEncoding == null)
throw new ArgumentNullException();
if (textWriter is StringWriter)
innerEncoding = PlatformCompatibleUnicode;
innerEncoding = textWriter.Encoding ?? throw new ArgumentException(string.Format("No encoding for {0}", textWriter));
return outerEncoding.IsPlatformCompatibleUnicode()
? new TextWriterStream(textWriter, leaveOpen)
: Encoding.CreateTranscodingStream(new TextWriterStream(textWriter, leaveOpen), innerEncoding, outerEncoding, false);
sealed class TextWriterStream : Stream
Nullable<byte> lastByte = null;
public TextWriterStream(TextWriter textWriter, bool leaveOpen) => (this.textWriter, this.leaveOpen) = (textWriter ?? throw new ArgumentNullException(), leaveOpen);
public override bool CanRead => false;
public override bool CanSeek => false;
public override bool CanWrite => true;
public override long Length => throw new NotSupportedException();
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
public override int Read(Span<byte> buffer) => throw new NotSupportedException();
public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException();
public override int ReadByte() => throw new NotSupportedException();
bool TryPopLastHalfChar(byte b, out char ch)
Span<byte> tempBuffer = stackalloc byte [2];
tempBuffer[0] = lastByte.Value; tempBuffer[1] = b;
ch = MemoryMarshal.Cast<byte, char>(tempBuffer)[0];
void PushLastHalfChar(byte b)
throw new InvalidOperationException("Last half character is already saved.");
throw new ObjectDisposedException(GetType().Name);
static void Flush(TextWriter textWriter, Nullable<byte> lastByte)
throw new InvalidOperationException(string.Format("Attempt to flush writer with pending byte {0}", (int)lastByte));
static Task FlushAsync(TextWriter textWriter, Nullable<byte> lastByte, CancellationToken cancellationToken)
throw new InvalidOperationException("Attempt to flush writer with pending byte");
if (cancellationToken.IsCancellationRequested)
return Task.FromCanceled(cancellationToken);
return textWriter.FlushAsync();
public override void Flush()
Flush(textWriter, lastByte);
public override Task FlushAsync(CancellationToken cancellationToken)
return FlushAsync(textWriter, lastByte, cancellationToken);
public override void Write(byte[] buffer, int offset, int count)
ValidateBufferArgs(buffer, offset, count);
Write(buffer.AsSpan(offset, count));
public override void Write(ReadOnlySpan<byte> buffer)
if (TryPopLastHalfChar(buffer[0], out var ch))
buffer = buffer.Slice(1);
if (buffer.Length % 2 != 0)
PushLastHalfChar(buffer[buffer.Length - 1]);
buffer = buffer.Slice(0, buffer.Length - 1);
textWriter.Write(MemoryMarshal.Cast<byte, char>(buffer));
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
ValidateBufferArgs(buffer, offset, count);
return WriteAsync(buffer.AsMemory(offset, count)).AsTask();
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
if (cancellationToken.IsCancellationRequested)
return ValueTask.FromCanceled(cancellationToken);
return WriteAsyncCore(buffer, cancellationToken);
catch (OperationCanceledException oce)
return new ValueTask(Task.FromCanceled(oce.CancellationToken));
catch (Exception exception)
return ValueTask.FromException(exception);
async ValueTask WriteAsyncCore(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken)
if (TryPopLastHalfChar(buffer.Span[0], out var ch))
await textWriter.WriteAsync(ch);
buffer = buffer.Slice(1);
if (buffer.Length % 2 != 0)
PushLastHalfChar(buffer.Span[buffer.Length - 1]);
buffer = buffer.Slice(0, buffer.Length - 1);
await textWriter.WriteAsync(Utils.Cast<byte, char>(buffer), cancellationToken);
protected override void Dispose(bool disposing)
var textWriter = Interlocked.Exchange(ref this.textWriter!, null);
Flush(textWriter, lastByte);
public override async ValueTask DisposeAsync()
var textWriter = Interlocked.Exchange(ref this.textWriter!, null);
await FlushAsync(textWriter, lastByte, CancellationToken.None);
await textWriter.DisposeAsync();
await base.DisposeAsync();
static void ValidateBufferArgs(byte[] buffer, int offset, int count)
throw new ArgumentNullException(nameof(buffer));
if (offset < 0 || count < 0)
throw new ArgumentOutOfRangeException();
if (count > buffer.Length - offset)
throw new ArgumentException();
public static class Utils
public static ReadOnlyMemory<TTo> Cast<TFrom, TTo>(ReadOnlyMemory<TFrom> from)
if (typeof(TFrom) == typeof(TTo)) return (ReadOnlyMemory<TTo>)(object)from;
return new CastMemoryManager<TFrom, TTo>(MemoryMarshal.AsMemory(from)).Memory;
private sealed class CastMemoryManager<TFrom, TTo> : MemoryManager<TTo>
private readonly Memory<TFrom> _from;
public CastMemoryManager(Memory<TFrom> from) => _from = from;
public override Span<TTo> GetSpan() => MemoryMarshal.Cast<TFrom, TTo>(_from.Span);
protected override void Dispose(bool disposing) { }
public override MemoryHandle Pin(int elementIndex = 0) => throw new NotSupportedException();
public override void Unpin() => throw new NotSupportedException();
static string hokke = "\U00029E3D";
public static async Task Test()
Console.WriteLine("Platform encoding: {0} ({1})", TextExtensions.PlatformCompatibleUnicode.EncodingName, TextExtensions.PlatformCompatibleUnicode.CodePage);
TestSimple("a"+hokke+"hello");
TestJsonSerializer(new { Hello = "There" }, true);
await TestJsonSerializerAsync(new { Hello = "There"+hokke });
await TestJsonSerializerAsync(Enumerable.Range(0, 1000).Select(i => new { Id = i, Hello = "There"+hokke }));
public static void TestSimple(string s)
TestSimple(s, new UnicodeEncoding(false, false));
TestSimple(s, new UTF8Encoding(false));
TestSimple(s, new UTF32Encoding(!BitConverter.IsLittleEndian, false));
TestSimple(s, new UTF32Encoding(BitConverter.IsLittleEndian, false));
public static void TestSimple(string s, Encoding outerEncoding)
var sb = new StringBuilder();
using (TextWriter innerWriter = new StringWriter(sb))
using (var stream = innerWriter.AsWrappedWriteOnlyStream(outerEncoding))
using (var outerWriter = new StreamWriter(stream, outerEncoding))
Assert.AreEqual(s, s2, s);
public static void TestJsonSerializer<T>(T data, bool print = false)
var json1 = JsonSerializer.Serialize(data);
var sb = new StringBuilder();
using (TextWriter innerWriter = new StringWriter(sb))
JsonSerializerExtensions.Serialize(innerWriter, data);
var json2 = sb.ToString();
Console.WriteLine(json2);
Assert.AreEqual(json1, json2, json1);
public static async Task TestJsonSerializerAsync<T>(T data)
var json1 = JsonSerializer.Serialize(data);
var sb = new StringBuilder();
using (TextWriter innerWriter = new StringWriter(sb))
await JsonSerializerExtensions.SerializeAsync(innerWriter, data);
var json2 = sb.ToString();
Assert.AreEqual(json1, json2, json1);
public static async Task Main(string[] args)
Console.WriteLine("Environment version: {0} ({1}), {2}, LittleEndian = {3}", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription , Environment.Version, Environment.OSVersion, BitConverter.IsLittleEndian);
Console.WriteLine("{0} version: {1}", typeof(JsonSerializer).Assembly.GetName().Name, typeof(JsonSerializer).Assembly.FullName);
Console.WriteLine("Failed with unhandled exception: ");