Skip to content

Commit 6736f24

Browse files
niemyjskiCopilot
andcommitted
Fix serializer regressions and preserve raw data markers
- preserve literal JSON-looking strings in DataDictionary as strings - restore raw JSON emission only for values produced from structured data - preserve raw JSON markers through MessagePack storage roundtrips - coerce primitive SettingsDictionary JSON values to strings like main - keep dictionary depth-limit regression coverage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent c345828 commit 6736f24

8 files changed

Lines changed: 104 additions & 8 deletions

File tree

src/Exceptionless/Extensions/DataDictionaryExtensions.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,10 @@ public static void AddObject(this IData data, ExtendedDataInfo info, Exceptionle
135135
if (String.IsNullOrEmpty(json))
136136
return;
137137

138-
data.Data[name] = json;
138+
if (dataType.IsPrimitiveType())
139+
data.Data[name] = json;
140+
else
141+
data.Data.SetRawJson(name, json);
139142
}
140143
}
141144
}

src/Exceptionless/Models/Collections/DataDictionary.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,38 @@
33

44
namespace Exceptionless.Models {
55
public class DataDictionary : Dictionary<string, object> {
6+
private readonly HashSet<string> _rawJsonKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
7+
68
public DataDictionary() : base(StringComparer.OrdinalIgnoreCase) {}
79

810
public DataDictionary(IEnumerable<KeyValuePair<string, object>> values) : base(StringComparer.OrdinalIgnoreCase) {
911
foreach (var kvp in values)
1012
Add(kvp.Key, kvp.Value);
1113
}
1214

15+
public new object this[string key] {
16+
get => base[key];
17+
set {
18+
ClearRawJson(key);
19+
base[key] = value;
20+
}
21+
}
22+
23+
public new void Add(string key, object value) {
24+
ClearRawJson(key);
25+
base.Add(key, value);
26+
}
27+
28+
public new bool Remove(string key) {
29+
ClearRawJson(key);
30+
return base.Remove(key);
31+
}
32+
33+
public new void Clear() {
34+
_rawJsonKeys.Clear();
35+
base.Clear();
36+
}
37+
1338
public object GetValueOrDefault(string key) {
1439
return TryGetValue(key, out object value) ? value : null;
1540
}
@@ -35,5 +60,21 @@ public string GetString(string name, string @default) {
3560

3661
return String.Empty;
3762
}
63+
64+
internal bool IsRawJson(string key) {
65+
return !String.IsNullOrEmpty(key) && _rawJsonKeys.Contains(key);
66+
}
67+
68+
internal void SetRawJson(string key, string value) {
69+
base[key] = value;
70+
71+
if (!String.IsNullOrEmpty(key))
72+
_rawJsonKeys.Add(key);
73+
}
74+
75+
private void ClearRawJson(string key) {
76+
if (!String.IsNullOrEmpty(key))
77+
_rawJsonKeys.Remove(key);
78+
}
3879
}
3980
}

src/Exceptionless/Properties/AssemblyInfo.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@
1010
[assembly: Exceptionless.Linking.Preserve]
1111
[assembly: Guid("2d458cc4-3bb3-4852-b6a2-11d5ac8672df")]
1212

13+
[assembly: InternalsVisibleTo("Exceptionless.MessagePack, PublicKey=0024000004800000940000000602000000240000525341310004000001000100a9357232b9bcad78fd310297fdb41bf42816ee2ca9ccdace999889de2badb6f06df2de1d9f2c8cb17b21f5311f11d6bb328d55e0dd9fe8adc5e2dc4610028c1bdacb3355d2e239b81d0bb0ac83e615fc641f8a3ec49e4fad8e305994953d448ef7b38e8c256601e54af19c035b562e3e5e5461c2a93b8dd11936e451b05034a2")]
1314
[assembly: InternalsVisibleTo("Exceptionless.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100a9357232b9bcad78fd310297fdb41bf42816ee2ca9ccdace999889de2badb6f06df2de1d9f2c8cb17b21f5311f11d6bb328d55e0dd9fe8adc5e2dc4610028c1bdacb3355d2e239b81d0bb0ac83e615fc641f8a3ec49e4fad8e305994953d448ef7b38e8c256601e54af19c035b562e3e5e5461c2a93b8dd11936e451b05034a2")]
1415
[assembly: InternalsVisibleTo("Exceptionless.TestHarness, PublicKey=0024000004800000940000000602000000240000525341310004000001000100a9357232b9bcad78fd310297fdb41bf42816ee2ca9ccdace999889de2badb6f06df2de1d9f2c8cb17b21f5311f11d6bb328d55e0dd9fe8adc5e2dc4610028c1bdacb3355d2e239b81d0bb0ac83e615fc641f8a3ec49e4fad8e305994953d448ef7b38e8c256601e54af19c035b562e3e5e5461c2a93b8dd11936e451b05034a2")]

src/Exceptionless/Serializer/DataDictionaryConverter.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public override DataDictionary Read(ref Utf8JsonReader reader, Type typeToConver
3333
case JsonTokenType.StartArray:
3434
// Store complex values as JSON strings (legacy behavior)
3535
using (var doc = JsonDocument.ParseValue(ref reader))
36-
dictionary[key] = doc.RootElement.GetRawText();
36+
dictionary.SetRawJson(key, doc.RootElement.GetRawText());
3737
break;
3838
case JsonTokenType.String:
3939
dictionary[key] = reader.GetString();
@@ -72,7 +72,7 @@ public override void Write(Utf8JsonWriter writer, DataDictionary value, JsonSeri
7272
writer.WritePropertyName(kvp.Key);
7373
if (kvp.Value == null) {
7474
writer.WriteNullValue();
75-
} else if (kvp.Value is string str && str.Length > 0 && (str[0] == '{' || str[0] == '[')) {
75+
} else if (value.IsRawJson(kvp.Key) && kvp.Value is string str && str.Length > 0 && (str[0] == '{' || str[0] == '[')) {
7676
// String values that contain JSON (from roundtripping through storage)
7777
// must be emitted as raw JSON objects, not escaped strings.
7878
try {

src/Exceptionless/Serializer/SettingsDictionaryConverter.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,16 @@ public override SettingsDictionary Read(ref Utf8JsonReader reader, Type typeToCo
2626
if (!reader.Read())
2727
throw new JsonException("Unexpected end of JSON");
2828

29-
string value = reader.TokenType == JsonTokenType.Null ? null : reader.GetString();
29+
string value;
30+
if (reader.TokenType == JsonTokenType.Null) {
31+
value = null;
32+
} else if (reader.TokenType == JsonTokenType.String) {
33+
value = reader.GetString();
34+
} else {
35+
using (var doc = JsonDocument.ParseValue(ref reader))
36+
value = doc.RootElement.GetRawText();
37+
}
38+
3039
dictionary[key] = value;
3140
}
3241

src/Platforms/Exceptionless.MessagePack/DataDictionaryFormatter.cs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
using System.Collections.Generic;
1+
using System;
2+
using System.Collections.Generic;
23
using Exceptionless.Models;
34
using Exceptionless.Models.Data;
45
using MessagePack;
56
using MessagePack.Formatters;
67

78
namespace Exceptionless.MessagePack {
89
internal class DataDictionaryFormatter : IMessagePackFormatter<DataDictionary?> {
10+
private const string RawJsonPrefix = "\u001Eexceptionless:raw-json:";
11+
912
public void Serialize(ref MessagePackWriter writer, DataDictionary? value, MessagePackSerializerOptions options) {
1013
if (value == null) {
1114
writer.WriteNil();
@@ -58,8 +61,13 @@ public void Serialize(ref MessagePackWriter writer, DataDictionary? value, Messa
5861
writer.Write((string)item.Value);
5962
break;
6063
default:
61-
options.Resolver.GetFormatter<object>()
62-
.Serialize(ref writer, item.Value, options);
64+
if (value.IsRawJson(item.Key) && item.Value is string rawJson) {
65+
writer.Write(RawJsonPrefix + rawJson);
66+
} else {
67+
options.Resolver.GetFormatter<object>()
68+
.Serialize(ref writer, item.Value, options);
69+
}
70+
6371
break;
6472
}
6573
}
@@ -138,7 +146,10 @@ public void Serialize(ref MessagePackWriter writer, DataDictionary? value, Messa
138146
#endif
139147
default: {
140148
var value = options.Resolver.GetFormatter<object>().Deserialize(ref reader, options);
141-
dic.Add(key, value);
149+
if (value is string rawJson && rawJson.StartsWith(RawJsonPrefix, StringComparison.Ordinal))
150+
dic.SetRawJson(key, rawJson.Substring(RawJsonPrefix.Length));
151+
else
152+
dic.Add(key, value);
142153
break;
143154
}
144155
}

test/Exceptionless.Tests/Serializer/Models/DataDictionarySerializerTests.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Text.Json;
23
using Exceptionless.Models;
34
using Exceptionless.Tests.Serializer;
45
using Xunit;
@@ -107,5 +108,22 @@ public void Deserialize_DataDictionary_JArrayValuesBecomeCompactStrings() {
107108
// Assert
108109
Assert.Equal(expected, data["items"]);
109110
}
111+
112+
[Fact]
113+
public void Serialize_DataDictionary_LiteralJsonString_RemainsString() {
114+
// Regression test: literal string values that happen to look like JSON
115+
// must stay strings on the wire. Only values produced from complex objects
116+
// should be emitted as raw JSON.
117+
var data = new DataDictionary {
118+
["payload"] = """{"a":1}""",
119+
["items"] = """[1,2]"""
120+
};
121+
122+
string json = Serialize(data);
123+
124+
using var doc = JsonDocument.Parse(json);
125+
Assert.Equal("""{"a":1}""", doc.RootElement.GetProperty("payload").GetString());
126+
Assert.Equal("[1,2]", doc.RootElement.GetProperty("items").GetString());
127+
}
110128
}
111129
}

test/Exceptionless.Tests/Serializer/Models/SettingsDictionarySerializerTests.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,5 +100,18 @@ public void Deserialize_SettingsDictionary_GetInt32ParsesValues() {
100100
Assert.Equal(25, settings.GetInt32("max_events"));
101101
Assert.Equal(99, settings.GetInt32("invalid", 99));
102102
}
103+
104+
[Fact]
105+
public void Deserialize_SettingsDictionary_PrimitiveJsonValuesBecomeStrings() {
106+
// Regression test: main coerces primitive JSON values into strings when
107+
// deserializing SettingsDictionary. The STJ converter must preserve that behavior.
108+
const string json = """{"max_events":25,"is_enabled":true,"threshold":3.14}""";
109+
110+
SettingsDictionary settings = Deserialize<SettingsDictionary>(json);
111+
112+
Assert.Equal("25", settings["max_events"]);
113+
Assert.Equal("true", settings["is_enabled"]);
114+
Assert.Equal("3.14", settings["threshold"]);
115+
}
103116
}
104117
}

0 commit comments

Comments
 (0)