diff --git a/src/Seq.Syntax/Expressions/Runtime/LevelValue.cs b/src/Seq.Syntax/Expressions/Runtime/LevelValue.cs
index f9d3f53..a67f5a8 100644
--- a/src/Seq.Syntax/Expressions/Runtime/LevelValue.cs
+++ b/src/Seq.Syntax/Expressions/Runtime/LevelValue.cs
@@ -1,4 +1,4 @@
-// Copyright © Serilog Contributors
+// Copyright © Datalust and Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+using System.Text.Json.Serialization;
+
namespace Seq.Syntax.Expressions.Runtime;
///
@@ -19,9 +21,13 @@ namespace Seq.Syntax.Expressions.Runtime;
/// the document's @l (Information when absent). Distinguishes levels from plain
/// strings so that fixed-width moniker formats apply.
///
+/// Serializes as its name, so that a level handed to a caller through
+/// degrades to a JSON string when cloned or written, rather than
+/// to an object carrying the wrapper's own fields.
+[JsonConverter(typeof(LevelValueJsonConverter))]
sealed class LevelValue(string name)
{
public string Name { get; } = name;
public override string ToString() => Name;
-}
+}
\ No newline at end of file
diff --git a/src/Seq.Syntax/Expressions/Runtime/LevelValueJsonConverter.cs b/src/Seq.Syntax/Expressions/Runtime/LevelValueJsonConverter.cs
new file mode 100644
index 0000000..0defe2c
--- /dev/null
+++ b/src/Seq.Syntax/Expressions/Runtime/LevelValueJsonConverter.cs
@@ -0,0 +1,31 @@
+// Copyright © Datalust and Contributors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Seq.Syntax.Expressions.Runtime;
+
+sealed class LevelValueJsonConverter : JsonConverter
+{
+ public override LevelValue Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ return LevelMapping.ToLevelValue(reader.GetString());
+ }
+
+ public override void Write(Utf8JsonWriter writer, LevelValue value, JsonSerializerOptions options)
+ {
+ writer.WriteStringValue(value.Name);
+ }
+}
\ No newline at end of file
diff --git a/src/Seq.Syntax/Expressions/Runtime/Values.cs b/src/Seq.Syntax/Expressions/Runtime/Values.cs
index 0d4cabe..710fd29 100644
--- a/src/Seq.Syntax/Expressions/Runtime/Values.cs
+++ b/src/Seq.Syntax/Expressions/Runtime/Values.cs
@@ -40,9 +40,9 @@ static class Values
{
switch (Underlying(value))
{
- // Typed scalars degrade to their JSON string forms on insertion; `DeepClone()` does
- // this for the serializer-supported ones, but a `LevelValue` would clone as a
- // `{"Name": ...}` object.
+ // Typed scalars degrade to their JSON string forms on insertion. `DeepClone()` does
+ // this too, via each type's serializer support, but for a `LevelValue` the direct
+ // conversion avoids a serializer round-trip.
case LevelValue level:
return JsonValue.Create(level.Name);
// `unsafe()` output has no JSON form to degrade to: `DeepClone()` would serialize
diff --git a/test/Seq.Syntax.Tests/Expressions/ValueMechanicsTests.cs b/test/Seq.Syntax.Tests/Expressions/ValueMechanicsTests.cs
index 5aed9e5..6ad28ce 100644
--- a/test/Seq.Syntax.Tests/Expressions/ValueMechanicsTests.cs
+++ b/test/Seq.Syntax.Tests/Expressions/ValueMechanicsTests.cs
@@ -48,6 +48,21 @@ public void ClrBackedValuesRoundTrip()
Assert.Equal(TimeSpan.FromMinutes(90), tsBack);
}
+ [Fact]
+ public void LevelValuesHandedToCallersCloneAsStrings()
+ {
+ // A caller re-parenting a `@Level` result, e.g. `seqcli search --column @Level`, sees a
+ // JSON string rather than the wrapper's fields.
+ var evt = new JsonObject { ["@l"] = "Warning" };
+ var expr = SeqExpression.Compile("@Level");
+
+ Assert.True(expr(evt).TryGetValue(out var level));
+ var enriched = new JsonObject { ["Column"] = level!.DeepClone() };
+
+ Assert.Equal("Warning", (string)enriched["Column"]!);
+ Assert.Equal("""{"Column":"Warning"}""", enriched.ToJsonString());
+ }
+
[Fact]
public void CallablesCanBeWrappedAndRecovered()
{