// Copyright (c) Microsoft. All rights reserved. using System; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; namespace Microsoft.Agents.AI.Workflows.Checkpointing; /// /// Provides special handling for serialization and deserialization, enabling delayed deserialization /// of the inner value. This is used to enable serialization/deserialization of objects whose type information is not available /// at the time of initial deserialization, e.g. user-defined state types. /// /// This operates in conjuction with and to abstract /// away the speicfics of a given serialization format in favor of and /// and related methods. /// /// internal sealed class PortableValueConverter(JsonMarshaller marshaller) : JsonConverter { public override PortableValue? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { SequencePosition initial = reader.Position; JsonTypeInfo baseTypeInfo = WorkflowsJsonUtilities.JsonContext.Default.PortableValue; PortableValue? maybeValue = JsonSerializer.Deserialize(ref reader, baseTypeInfo); if (maybeValue is null) { throw new JsonException($"Could not deserialize a PortableValue from JSON at position {initial}."); } else if (maybeValue.Value is JsonElement element) { // This happens when we do not have the type information available to deserialize the value directly. // We need to wrap it in a JsonWireSerializedValue so that we can deserialize it return new PortableValue(maybeValue.TypeId, new JsonWireSerializedValue(marshaller, element)); } else if (maybeValue.TypeId.IsMatch(maybeValue.Value.GetType())) { return maybeValue; } throw new JsonException($"Deserialized PortableValue contains a value of type {maybeValue.Value.GetType()} which does not match the expected type {maybeValue.TypeId} at position {initial}."); } public override void Write(Utf8JsonWriter writer, PortableValue value, JsonSerializerOptions options) { PortableValue proxyValue; if (value.IsDelayedDeserialization && !value.IsDeserialized) { if (value.Value is JsonWireSerializedValue jsonWireValue) { proxyValue = new(value.TypeId, jsonWireValue.Data); } else { // Users should never see this unless they're trying to cross wire formats throw new InvalidOperationException("Cannot serialize a PortableValue that has not been deserialized. Please deserialize it with .As/AsType() or Is/IsType() methods first."); } } else { JsonElement element = marshaller.Marshal(value.Value, value.Value.GetType()); proxyValue = new(value.TypeId, element); } JsonTypeInfo baseTypeInfo = WorkflowsJsonUtilities.JsonContext.Default.PortableValue; JsonSerializer.Serialize(writer, proxyValue, baseTypeInfo); } }