Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .changeset/quiet-hedgehogs-nest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'com.posthog.unity': patch
---

Omit null-valued custom object properties recursively when serializing events for delivery or disk storage, while preserving null array positions and existing generic serialization behavior.
77 changes: 66 additions & 11 deletions com.posthog.unity/Runtime/Utilities/JsonSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,23 @@ static void SerializeEventToBuilder(PostHogEvent evt, StringBuilder sb)
sb.Append(",\"timestamp\":");
AppendEscapedString(evt.Timestamp, sb);
sb.Append(",\"properties\":");
SerializeValue(evt.Properties, sb);
if (evt.Properties == null)
{
sb.Append("null");
}
else
{
SerializeDictionary(
evt.Properties,
sb,
omitNullMembers: true,
preserveExceptionMetadata: evt.Event == "$exception"
);
}
sb.Append('}');
}

static void SerializeValue(object value, StringBuilder sb)
static void SerializeValue(object value, StringBuilder sb, bool omitNullMembers = false)
{
if (value == null)
{
Expand Down Expand Up @@ -101,13 +113,13 @@ static void SerializeValue(object value, StringBuilder sb)
AppendEscapedString(dto.ToString("o"), sb);
break;
case IDictionary<string, object> dict:
SerializeDictionary(dict, sb);
SerializeDictionary(dict, sb, omitNullMembers);
break;
case IDictionary genericDict:
SerializeGenericDictionary(genericDict, sb);
SerializeGenericDictionary(genericDict, sb, omitNullMembers);
break;
case IList list:
SerializeList(list, sb);
SerializeList(list, sb, omitNullMembers);
break;
default:
// For other types, try to convert to string
Expand All @@ -116,50 +128,93 @@ static void SerializeValue(object value, StringBuilder sb)
}
}

static void SerializeDictionary(IDictionary<string, object> dict, StringBuilder sb)
// Resolve only SerializeValue's fallback conversion before deciding whether to emit a member.
// Passing the resulting string to SerializeValue avoids calling the original ToString twice.
static object ResolveObjectMemberValue(object value) =>
value switch
{
null
or string
or bool
or int
or long
or float
or double
or decimal
or DateTime
or DateTimeOffset
or IDictionary<string, object>
or IDictionary
or IList => value,
_ => value.ToString(),
};

static void SerializeDictionary(
IDictionary<string, object> dict,
StringBuilder sb,
bool omitNullMembers,
bool preserveExceptionMetadata = false
)
{
sb.Append('{');
bool first = true;
foreach (var kvp in dict)
{
// Exception frames have their own nullable schema; custom siblings still normalize.
bool omitNestedNulls =
omitNullMembers && !(preserveExceptionMetadata && kvp.Key == "$exception_list");
var value = omitNestedNulls ? ResolveObjectMemberValue(kvp.Value) : kvp.Value;
if (omitNestedNulls && value == null)
{
continue;
}
if (!first)
{
sb.Append(',');
}
first = false;
AppendEscapedString(kvp.Key, sb);
sb.Append(':');
SerializeValue(kvp.Value, sb);
SerializeValue(value, sb, omitNestedNulls);
}
sb.Append('}');
}

static void SerializeGenericDictionary(IDictionary dict, StringBuilder sb)
static void SerializeGenericDictionary(
IDictionary dict,
StringBuilder sb,
bool omitNullMembers
)
{
sb.Append('{');
bool first = true;
foreach (DictionaryEntry entry in dict)
{
var value = omitNullMembers ? ResolveObjectMemberValue(entry.Value) : entry.Value;
if (omitNullMembers && value == null)
{
continue;
}
if (!first)
{
sb.Append(',');
}
first = false;
AppendEscapedString(entry.Key?.ToString() ?? "", sb);
sb.Append(':');
SerializeValue(entry.Value, sb);
SerializeValue(value, sb, omitNullMembers);
}
sb.Append('}');
}

static void SerializeList(IList list, StringBuilder sb)
static void SerializeList(IList list, StringBuilder sb, bool omitNullMembers)
{
sb.Append("[");
for (int i = 0; i < list.Count; i++)
{
if (i > 0)
sb.Append(",");
SerializeValue(list[i], sb);
SerializeValue(list[i], sb, omitNullMembers);
}
sb.Append("]");
}
Expand Down
Loading