Skip to content
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 @@ -13,7 +13,13 @@ internal sealed class JsonMarshaller : IWireMarshaller<JsonElement>

public JsonMarshaller(JsonSerializerOptions? serializerOptions = null)
{
this._internalOptions = new JsonSerializerOptions(WorkflowsJsonUtilities.DefaultOptions);
this._internalOptions = new JsonSerializerOptions(WorkflowsJsonUtilities.DefaultOptions)
{
// Propagate from the user-provided options if set; enables support for databases
// like PostgreSQL jsonb that do not preserve property order.
AllowOutOfOrderMetadataProperties = serializerOptions?.AllowOutOfOrderMetadataProperties is true,
};

this._internalOptions.Converters.Add(new PortableValueConverter(this));
this._internalOptions.Converters.Add(new ExecutorIdentityConverter());
this._internalOptions.Converters.Add(new ScopeKeyConverter());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -672,4 +672,104 @@ public async Task Test_InMemoryCheckpointManager_JsonRoundTripAsync()

ValidateCheckpoint(retrievedCheckpoint, prototype);
}

/// <summary>
/// Verifies that the default behavior (without AllowOutOfOrderMetadataProperties) fails
/// when $type metadata is not the first property, demonstrating the PostgreSQL jsonb issue.
/// See: https://github.com/microsoft/agent-framework/issues/2962
/// </summary>
[Fact]
public void Test_OutOfOrderMetadataProperties_WithoutOption_Fails()
{
// Arrange
JsonMarshaller marshaller = new();
EdgeInfo edgeInfo = TestEdgeInfo_DirectNoCondition;

// Serialize to JSON
JsonElement serialized = marshaller.Marshal(edgeInfo);
string json = serialized.GetRawText();

// Simulate PostgreSQL jsonb behavior: reorder properties so $type is not first
string reorderedJson = ReorderJsonPropertiesToMoveTypeDiscriminatorLast(json);

// Act & Assert - Without the option, deserialization should fail
JsonElement reorderedElement = JsonDocument.Parse(reorderedJson).RootElement;
Action act = () => marshaller.Marshal<EdgeInfo>(reorderedElement);

act.Should().Throw<JsonException>();
}

/// <summary>
/// Simulates PostgreSQL jsonb behavior where property order is not preserved,
/// causing $type metadata to not be the first property.
/// This test verifies that deserialization works when AllowOutOfOrderMetadataProperties is enabled.
/// See: https://github.com/microsoft/agent-framework/issues/2962
/// </summary>
[Fact]
public void Test_OutOfOrderMetadataProperties_WithOptionEnabled_Succeeds()
{
// Arrange
EdgeInfo edgeInfo = TestEdgeInfo_DirectNoCondition;

// Serialize to JSON using standard marshaller
JsonMarshaller marshaller = new();
JsonElement serialized = marshaller.Marshal(edgeInfo);
string json = serialized.GetRawText();

// Simulate PostgreSQL jsonb behavior: reorder properties so $type is not first
string reorderedJson = ReorderJsonPropertiesToMoveTypeDiscriminatorLast(json);
JsonElement reorderedElement = JsonDocument.Parse(reorderedJson).RootElement;

// Act - Deserialize with AllowOutOfOrderMetadataProperties enabled via JsonSerializerOptions
JsonSerializerOptions options = new() { AllowOutOfOrderMetadataProperties = true };
JsonMarshaller marshallerWithOption = new(options);
EdgeInfo deserialized = marshallerWithOption.Marshal<EdgeInfo>(reorderedElement);

// Assert
deserialized.Should().Match(edgeInfo.CreatePolyValidator());
}

private static string ReorderJsonPropertiesToMoveTypeDiscriminatorLast(string json)
{
// Parse JSON, extract $type, rebuild with $type at end
using JsonDocument doc = JsonDocument.Parse(json);
JsonElement root = doc.RootElement;

Dictionary<string, JsonElement> properties = [];
JsonElement? typeValue = null;

foreach (JsonProperty prop in root.EnumerateObject())
{
if (prop.Name == "$type")
{
typeValue = prop.Value.Clone();
}
else
{
properties[prop.Name] = prop.Value.Clone();
}
}

// Rebuild JSON with $type last
using System.IO.MemoryStream ms = new();
using (Utf8JsonWriter writer = new(ms))
{
writer.WriteStartObject();
foreach (KeyValuePair<string, JsonElement> kvp in properties)
{
writer.WritePropertyName(kvp.Key);
kvp.Value.WriteTo(writer);
}

if (typeValue.HasValue)
{
writer.WritePropertyName("$type");
typeValue.Value.WriteTo(writer);
}

writer.WriteEndObject();
}

return System.Text.Encoding.UTF8.GetString(ms.ToArray());
}
}
Loading