From e4a34dd3a0a31a2cd8dcceef0e8d3e39894b106e Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Thu, 3 Sep 2026 12:45:19 +1000 Subject: [PATCH 1/2] Add a `JsonConverter` for `LevelValue`, protecting against accidental leakage of the internal `LevelValue` type at API boundaries. Old case-by-case protections are still maintained, but the converter makes it harder to get things observably wrong. Before this change, `seqcli search --column @Level` (not yet in upstream `seqcli`) renders `{"Name": ...}` style output. After, it's just plain-old strings. Assisted-by: Claude:claude-fable-5-1 --- .../Expressions/Runtime/LevelValue.cs | 20 +++++++++++++++++++ src/Seq.Syntax/Expressions/Runtime/Values.cs | 6 +++--- .../Expressions/ValueMechanicsTests.cs | 15 ++++++++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/Seq.Syntax/Expressions/Runtime/LevelValue.cs b/src/Seq.Syntax/Expressions/Runtime/LevelValue.cs index f9d3f53..d46f37d 100644 --- a/src/Seq.Syntax/Expressions/Runtime/LevelValue.cs +++ b/src/Seq.Syntax/Expressions/Runtime/LevelValue.cs @@ -12,6 +12,9 @@ // 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; /// @@ -19,9 +22,26 @@ 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; } + +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); + } +} 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() { From 6d235fe18db10f270a23c716e90a6599781b2838 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Thu, 3 Sep 2026 12:54:26 +1000 Subject: [PATCH 2/2] One type per file --- .../Expressions/Runtime/LevelValue.cs | 18 ++--------- .../Runtime/LevelValueJsonConverter.cs | 31 +++++++++++++++++++ 2 files changed, 33 insertions(+), 16 deletions(-) create mode 100644 src/Seq.Syntax/Expressions/Runtime/LevelValueJsonConverter.cs diff --git a/src/Seq.Syntax/Expressions/Runtime/LevelValue.cs b/src/Seq.Syntax/Expressions/Runtime/LevelValue.cs index d46f37d..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,7 +12,6 @@ // 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; @@ -31,17 +30,4 @@ sealed class LevelValue(string name) public string Name { get; } = name; public override string ToString() => Name; -} - -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/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