diff --git a/src/StreamJsonRpc/IGenericTypeArgAssist.cs b/src/StreamJsonRpc/IGenericTypeArgAssist.cs
new file mode 100644
index 00000000..52c3e500
--- /dev/null
+++ b/src/StreamJsonRpc/IGenericTypeArgAssist.cs
@@ -0,0 +1,42 @@
+namespace StreamJsonRpc;
+
+///
+/// A non-generic interface with a generic method, for use with
+/// so that generic contexts can be preserved and reused later in a non-generic context.
+///
+internal interface IGenericTypeArgAssist
+{
+ ///
+ /// Invokes whatever functionality the implementation provides, using the specified generic type argument.
+ ///
+ /// A generic type argument, whose semantics are up to the implementation.
+ /// Optional state that the caller may have provided.
+ /// An arbitrary result, up to the implementation.
+ object? Invoke(object? state = null);
+}
+
+///
+/// A non-generic interface allowing invocation of a generic method with a type argument known only at runtime.
+///
+///
+internal interface IGenericTypeArgStore
+{
+ ///
+ /// Invokes the generic method.
+ ///
+ /// An implementation of .
+ /// Optional state that the caller may have provided.
+ /// An arbitrary result, up to the implementation.
+ object? Invoke(IGenericTypeArgAssist assist, object? state = null);
+}
+
+///
+/// A generic implementation of
+/// that can be created while in a generic context, so that a non-generic method can invoke another generic method later.
+///
+/// The generic type argument that must be supplied later.
+internal class GenericTypeArgStore : IGenericTypeArgStore
+{
+ ///
+ public object? Invoke(IGenericTypeArgAssist assist, object? state = null) => assist.Invoke(state);
+}
diff --git a/src/StreamJsonRpc/PolyTypeJsonFormatter.cs b/src/StreamJsonRpc/PolyTypeJsonFormatter.cs
new file mode 100644
index 00000000..eb816c0a
--- /dev/null
+++ b/src/StreamJsonRpc/PolyTypeJsonFormatter.cs
@@ -0,0 +1,1290 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Buffers;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.IO.Pipelines;
+using System.Reflection;
+using System.Runtime.ExceptionServices;
+using System.Runtime.Serialization;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
+using Nerdbank.Streams;
+using PolyType;
+using StreamJsonRpc.Protocol;
+using StreamJsonRpc.Reflection;
+
+namespace StreamJsonRpc;
+
+///
+/// A formatter that emits UTF-8 encoded JSON where user data should be serializable via the .
+/// This formatter is NativeAOT ready and relies on PolyType.
+///
+[Experimental("PolyTypeJson")]
+public partial class PolyTypeJsonFormatter : FormatterBase, IJsonRpcMessageFormatter, IJsonRpcMessageTextFormatter, IJsonRpcInstanceContainer, IJsonRpcMessageFactory, IJsonRpcFormatterTracingCallbacks
+{
+ private static readonly ProxyFactory ProxyFactory = ProxyFactory.NoDynamic;
+
+ private static readonly JsonWriterOptions WriterOptions = new() { };
+
+ private static readonly JsonDocumentOptions DocumentOptions = new() { };
+
+ ///
+ /// The to use for the envelope and built-in types.
+ ///
+ private static readonly JsonSerializerOptions BuiltInSerializerOptions = new()
+ {
+ TypeInfoResolver = SourceGenerationContext.Default,
+ Converters =
+ {
+ RequestIdSTJsonConverter.Instance,
+ },
+ };
+
+ ///
+ /// UTF-8 encoding without a preamble.
+ ///
+ private static readonly Encoding DefaultEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
+
+ private static readonly JsonRpcProxyOptions DefaultRpcMarshalableProxyOptions = new JsonRpcProxyOptions(JsonRpcProxyOptions.Default) { AcceptProxyWithExtraInterfaces = true, IsFrozen = true };
+
+ private readonly Dictionary genericLifts = [];
+
+ private readonly ToStringHelper serializationToStringHelper = new ToStringHelper();
+
+ private JsonSerializerOptions massagedUserDataSerializerOptions;
+
+ ///
+ /// Retains the message currently being deserialized so that it can be disposed when we're done with it.
+ ///
+ private JsonDocument? deserializingDocument;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public PolyTypeJsonFormatter()
+ {
+ // Prepare for built-in behavior.
+ this.RegisterGenericType();
+
+ // Take care with any options set *here* instead of in MassageUserDataSerializerOptions,
+ // because any settings made only here will be erased if the user changes the JsonSerializerOptions property.
+ this.massagedUserDataSerializerOptions = this.MassageUserDataSerializerOptions(new()
+ {
+ });
+ }
+
+ ///
+ /// Gets the shape provider for user data types.
+ ///
+ public required ITypeShapeProvider TypeShapeProvider { get; init; }
+
+ ///
+ public Encoding Encoding
+ {
+ get => DefaultEncoding;
+ set => throw new NotSupportedException();
+ }
+
+ ///
+ /// Gets or sets the options to use when serializing and deserializing JSON containing user data.
+ ///
+ public JsonSerializerOptions JsonSerializerOptions
+ {
+ get => this.massagedUserDataSerializerOptions;
+ set => this.massagedUserDataSerializerOptions = this.MassageUserDataSerializerOptions(new(value));
+ }
+
+ ///
+ public JsonRpcMessage Deserialize(ReadOnlySequence contentBuffer) => this.Deserialize(contentBuffer, this.Encoding);
+
+ ///
+ public JsonRpcMessage Deserialize(ReadOnlySequence contentBuffer, Encoding encoding)
+ {
+ if (encoding is not UTF8Encoding)
+ {
+ throw new NotSupportedException("Only our default encoding is supported.");
+ }
+
+ JsonDocument document = this.deserializingDocument = JsonDocument.Parse(contentBuffer, DocumentOptions);
+ if (document.RootElement.ValueKind != JsonValueKind.Object)
+ {
+ throw new JsonException("Expected a JSON object at the root of the message.");
+ }
+
+ JsonRpcMessage message;
+ if (document.RootElement.TryGetProperty(Utf8Strings.method, out JsonElement methodElement))
+ {
+ JsonRpcRequest request = new(this)
+ {
+ RequestId = ReadRequestId(),
+ Method = methodElement.GetString(),
+ JsonArguments = document.RootElement.TryGetProperty(Utf8Strings.@params, out JsonElement paramsElement) ? paramsElement : null,
+ TraceParent = document.RootElement.TryGetProperty(Utf8Strings.traceparent, out JsonElement traceParentElement) ? traceParentElement.GetString() : null,
+ TraceState = document.RootElement.TryGetProperty(Utf8Strings.tracestate, out JsonElement traceStateElement) ? traceStateElement.GetString() : null,
+ };
+ message = request;
+ }
+ else if (document.RootElement.TryGetProperty(Utf8Strings.result, out JsonElement resultElement))
+ {
+ JsonRpcResult result = new(this)
+ {
+ RequestId = ReadRequestId(),
+ JsonResult = resultElement,
+ };
+ message = result;
+ }
+ else if (document.RootElement.TryGetProperty(Utf8Strings.error, out JsonElement errorElement))
+ {
+ JsonRpcError error = new(this)
+ {
+ RequestId = ReadRequestId(),
+ Error = new JsonRpcError.ErrorDetail(this)
+ {
+ Code = (JsonRpcErrorCode)errorElement.GetProperty(Utf8Strings.code).GetInt64(),
+ Message = errorElement.GetProperty(Utf8Strings.message).GetString(),
+ JsonData = errorElement.TryGetProperty(Utf8Strings.data, out JsonElement dataElement) ? dataElement : null,
+ },
+ };
+
+ message = error;
+ }
+ else
+ {
+ throw new JsonException("Expected a request, result, or error message.");
+ }
+
+ message.Version = document.RootElement.TryGetProperty(Utf8Strings.jsonrpc, out JsonElement jsonRpcElement)
+ ? (jsonRpcElement.ValueEquals(Utf8Strings.v2_0) ? "2.0" : (jsonRpcElement.GetString() ?? throw new JsonException("Unexpected null value for jsonrpc property.")))
+ : "1.0";
+
+ if (message is IMessageWithTopLevelPropertyBag messageWithTopLevelPropertyBag)
+ {
+ messageWithTopLevelPropertyBag.TopLevelPropertyBag = new TopLevelPropertyBag(document, this.massagedUserDataSerializerOptions);
+ }
+
+ RequestId ReadRequestId()
+ {
+ return document.RootElement.TryGetProperty(Utf8Strings.id, out JsonElement idElement)
+ ? idElement.Deserialize(SourceGenerationContext.Default.RequestId)
+ : RequestId.NotSpecified;
+ }
+
+ IJsonRpcTracingCallbacks? tracingCallbacks = this.JsonRpc;
+ tracingCallbacks?.OnMessageDeserialized(message, document.RootElement);
+
+ this.TryHandleSpecialIncomingMessage(message);
+
+ return message;
+ }
+
+ ///
+ public object GetJsonText(JsonRpcMessage message) => throw new NotSupportedException();
+
+ ///
+ public void Serialize(IBufferWriter bufferWriter, JsonRpcMessage message)
+ {
+ Requires.NotNull(message);
+
+ using (this.TrackSerialization(message))
+ {
+ try
+ {
+ using Utf8JsonWriter writer = new(bufferWriter, WriterOptions);
+ writer.WriteStartObject();
+ WriteVersion();
+ switch (message)
+ {
+ case Protocol.JsonRpcRequest request:
+ WriteId(request.RequestId);
+ writer.WriteString(Utf8Strings.method, request.Method);
+ WriteArguments(request);
+ if (request.TraceParent is not null)
+ {
+ writer.WriteString(Utf8Strings.traceparent, request.TraceParent);
+ }
+
+ if (request.TraceState is not null)
+ {
+ writer.WriteString(Utf8Strings.tracestate, request.TraceState);
+ }
+
+ break;
+ case Protocol.JsonRpcResult result:
+ WriteId(result.RequestId);
+ WriteResult(result);
+ break;
+ case Protocol.JsonRpcError error:
+ WriteId(error.RequestId);
+ WriteError(error);
+ break;
+ default:
+ throw new ArgumentException("Unknown message type: " + message.GetType().Name, nameof(message));
+ }
+
+ if (message is IMessageWithTopLevelPropertyBag { TopLevelPropertyBag: TopLevelPropertyBag propertyBag })
+ {
+ propertyBag.WriteProperties(writer);
+ }
+
+ writer.WriteEndObject();
+
+ void WriteVersion()
+ {
+ switch (message.Version)
+ {
+ case "1.0":
+ // The 1.0 protocol didn't include the version property at all.
+ break;
+ case "2.0":
+ writer.WriteString(Utf8Strings.jsonrpc, Utf8Strings.v2_0);
+ break;
+ default:
+ writer.WriteString(Utf8Strings.jsonrpc, message.Version);
+ break;
+ }
+ }
+
+ void WriteId(RequestId id)
+ {
+ if (!id.IsEmpty)
+ {
+ writer.WritePropertyName(Utf8Strings.id);
+ RequestIdSTJsonConverter.Instance.Write(writer, id, BuiltInSerializerOptions);
+ }
+ }
+
+ void WriteArguments(Protocol.JsonRpcRequest request)
+ {
+ if (request.ArgumentsList is not null)
+ {
+ writer.WriteStartArray(Utf8Strings.@params);
+ for (int i = 0; i < request.ArgumentsList.Count; i++)
+ {
+ WriteUserData(request.ArgumentsList[i], request.ArgumentListDeclaredTypes?[i]);
+ }
+
+ writer.WriteEndArray();
+ }
+ else if (request.NamedArguments is not null)
+ {
+ writer.WriteStartObject(Utf8Strings.@params);
+ foreach (KeyValuePair argument in request.NamedArguments)
+ {
+ writer.WritePropertyName(argument.Key);
+ WriteUserData(argument.Value, request.NamedArgumentDeclaredTypes?[argument.Key]);
+ }
+
+ writer.WriteEndObject();
+ }
+ else if (request.Arguments is not null)
+ {
+ // This is a custom named arguments object, so we'll just serialize it as-is.
+ writer.WritePropertyName(Utf8Strings.@params);
+ WriteUserData(request.Arguments, declaredType: null);
+ }
+ }
+
+ void WriteResult(Protocol.JsonRpcResult result)
+ {
+ writer.WritePropertyName(Utf8Strings.result);
+ WriteUserData(result.Result, result.ResultDeclaredType);
+ }
+
+ void WriteError(Protocol.JsonRpcError error)
+ {
+ if (error.Error is null)
+ {
+ throw new ArgumentException($"{nameof(error.Error)} property must be set.", nameof(message));
+ }
+
+ writer.WriteStartObject(Utf8Strings.error);
+ writer.WriteNumber(Utf8Strings.code, (int)error.Error.Code);
+ writer.WriteString(Utf8Strings.message, error.Error.Message);
+ if (error.Error.Data is not null)
+ {
+ writer.WritePropertyName(Utf8Strings.data);
+ WriteUserData(error.Error.Data, null);
+ }
+
+ writer.WriteEndObject();
+ }
+
+ void WriteUserData(object? value, Type? declaredType)
+ {
+ if (value is null)
+ {
+ writer.WriteNullValue();
+ }
+ else if (declaredType is not null && declaredType != typeof(void) && declaredType != typeof(object))
+ {
+ JsonTypeInfo typeInfo = this.GetTypeInfoFromBuiltInOrUser(declaredType);
+ JsonSerializer.Serialize(writer, value, typeInfo);
+ }
+ else
+ {
+ Type normalizedType = NormalizeType(value.GetType());
+ JsonTypeInfo typeInfo = this.GetTypeInfoFromBuiltInOrUser(normalizedType);
+ JsonSerializer.Serialize(writer, value, typeInfo);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ throw new JsonException(Resources.SerializationFailure, ex);
+ }
+ }
+ }
+
+ ///
+ /// Registers a type that may be used as a generic type argument for some generic value to be serialized,
+ /// such as or .
+ ///
+ /// The type argument to some generic type.
+ public void RegisterGenericType()
+ {
+ this.ThrowIfInitialized();
+ if (this.genericLifts.ContainsKey(typeof(T)))
+ {
+ return;
+ }
+
+ this.genericLifts.Add(typeof(T), new GenericTypeArgStore());
+ }
+
+ void IJsonRpcFormatterTracingCallbacks.OnSerializationComplete(JsonRpcMessage message, ReadOnlySequence encodedMessage)
+ {
+ IJsonRpcTracingCallbacks? tracingCallbacks = this.JsonRpc;
+ this.serializationToStringHelper.Activate(encodedMessage);
+ try
+ {
+ tracingCallbacks?.OnMessageSerialized(message, this.serializationToStringHelper);
+ }
+ finally
+ {
+ this.serializationToStringHelper.Deactivate();
+ }
+ }
+
+ Protocol.JsonRpcRequest IJsonRpcMessageFactory.CreateRequestMessage() => new JsonRpcRequest(this);
+
+ Protocol.JsonRpcError IJsonRpcMessageFactory.CreateErrorMessage() => new JsonRpcError(this);
+
+ Protocol.JsonRpcResult IJsonRpcMessageFactory.CreateResultMessage() => new JsonRpcResult(this);
+
+ ///
+ private protected override MessageFormatterRpcMarshaledContextTracker CreateMessageFormatterRpcMarshaledContextTracker(JsonRpc rpc) => new MessageFormatterRpcMarshaledContextTracker.PolyTypeShape(rpc, ProxyFactory, this, this.TypeShapeProvider);
+
+ private JsonTypeInfo GetTypeInfoFromBuiltInOrUser(Type type) => BuiltInSerializerOptions.TryGetTypeInfo(type, out JsonTypeInfo? typeInfo) ? typeInfo : this.massagedUserDataSerializerOptions.GetTypeInfo(type);
+
+ private JsonSerializerOptions MassageUserDataSerializerOptions(JsonSerializerOptions options)
+ {
+ // This is required for $/cancelRequest messages.
+ options.Converters.Add(RequestIdSTJsonConverter.Instance);
+
+ // Add support for exotic types.
+ options.Converters.Add(new ProgressConverterFactory(this));
+ options.Converters.Add(new AsyncEnumerableConverter(this));
+ options.Converters.Add(new RpcMarshalableConverterFactory(this));
+ options.Converters.Add(new DuplexPipeConverter(this));
+ options.Converters.Add(new PipeReaderConverter(this));
+ options.Converters.Add(new PipeWriterConverter(this));
+ options.Converters.Add(new StreamConverter(this));
+
+ // Add support for serializing exceptions.
+ options.Converters.Add(new ExceptionConverter(this));
+
+ return options;
+ }
+
+ private object? GenericMethodInvoke(Type typeArg, IGenericTypeArgAssist assist, object? state = null)
+ {
+ if (!this.genericLifts.TryGetValue(typeArg, out IGenericTypeArgStore? lift))
+ {
+ throw new NotImplementedException($"{nameof(this.RegisterGenericType)}() must be called first with type argument: {typeArg}.");
+ }
+
+ return lift.Invoke(assist, state);
+ }
+
+ private static class Utf8Strings
+ {
+#pragma warning disable SA1300 // Element should begin with upper-case letter
+ internal static ReadOnlySpan jsonrpc => "jsonrpc"u8;
+
+ internal static ReadOnlySpan v2_0 => "2.0"u8;
+
+ internal static ReadOnlySpan id => "id"u8;
+
+ internal static ReadOnlySpan method => "method"u8;
+
+ internal static ReadOnlySpan @params => "params"u8;
+
+ internal static ReadOnlySpan traceparent => "traceparent"u8;
+
+ internal static ReadOnlySpan tracestate => "tracestate"u8;
+
+ internal static ReadOnlySpan result => "result"u8;
+
+ internal static ReadOnlySpan error => "error"u8;
+
+ internal static ReadOnlySpan code => "code"u8;
+
+ internal static ReadOnlySpan message => "message"u8;
+
+ internal static ReadOnlySpan data => "data"u8;
+#pragma warning restore SA1300 // Element should begin with upper-case letter
+ }
+
+ private class TopLevelPropertyBag : TopLevelPropertyBagBase
+ {
+ private readonly JsonDocument? incomingMessage;
+ private readonly JsonSerializerOptions jsonSerializerOptions;
+
+ ///
+ /// Initializes a new instance of the class
+ /// for use with an incoming message.
+ ///
+ /// The incoming message.
+ /// The serializer options to use.
+ internal TopLevelPropertyBag(JsonDocument incomingMessage, JsonSerializerOptions jsonSerializerOptions)
+ : base(isOutbound: false)
+ {
+ this.incomingMessage = incomingMessage;
+ this.jsonSerializerOptions = jsonSerializerOptions;
+ }
+
+ ///
+ /// Initializes a new instance of the class
+ /// for use with an outcoming message.
+ ///
+ /// The serializer options to use.
+ internal TopLevelPropertyBag(JsonSerializerOptions jsonSerializerOptions)
+ : base(isOutbound: true)
+ {
+ this.jsonSerializerOptions = jsonSerializerOptions;
+ }
+
+ internal void WriteProperties(Utf8JsonWriter writer)
+ {
+ if (this.incomingMessage is not null)
+ {
+ // We're actually re-transmitting an incoming message (remote target feature).
+ // We need to copy all the properties that were in the original message.
+ // Don't implement this without enabling the tests for the scenario found in JsonRpcRemoteTargetPolyTypeJsonFormatterTests.cs.
+ // The tests fail for reasons even without this support, so there's work to do beyond just implementing this.
+ throw new NotImplementedException();
+ }
+ else
+ {
+ foreach (KeyValuePair property in this.OutboundProperties)
+ {
+ writer.WritePropertyName(property.Key);
+ JsonTypeInfo typeInfo = this.jsonSerializerOptions.GetTypeInfo(property.Value.DeclaredType);
+ JsonSerializer.Serialize(writer, property.Value.Value, typeInfo);
+ }
+ }
+ }
+
+ protected internal override bool TryGetTopLevelProperty(string name, [MaybeNull] out T value)
+ {
+ if (this.incomingMessage?.RootElement.TryGetProperty(name, out JsonElement serializedValue) is true)
+ {
+ value = serializedValue.Deserialize((JsonTypeInfo)this.jsonSerializerOptions.GetTypeInfo(typeof(T)));
+ return true;
+ }
+
+ value = default;
+ return false;
+ }
+ }
+
+ private class JsonRpcRequest : JsonRpcRequestBase
+ {
+ private readonly PolyTypeJsonFormatter formatter;
+
+ private int? argumentCount;
+
+ private JsonElement? jsonArguments;
+
+ internal JsonRpcRequest(PolyTypeJsonFormatter formatter)
+ {
+ this.formatter = formatter;
+ }
+
+ public override int ArgumentCount => this.argumentCount ?? base.ArgumentCount;
+
+ public override IEnumerable? ArgumentNames
+ {
+ get
+ {
+ return this.JsonArguments?.ValueKind is JsonValueKind.Object
+ ? this.JsonArguments.Value.EnumerateObject().Select(p => p.Name)
+ : null;
+ }
+ }
+
+ internal JsonElement? JsonArguments
+ {
+ get => this.jsonArguments;
+ init
+ {
+ this.jsonArguments = value;
+ if (value.HasValue)
+ {
+ this.argumentCount = CountArguments(value.Value);
+ }
+ }
+ }
+
+ public override ArgumentMatchResult TryGetTypedArguments(ReadOnlySpan parameters, Span