I was working on an analytics collector in Unreal and I wasn’t very happy with the system they have in place now, which uses function calls to record events. Each event is either a key/value pair or a key/dict pair, and you have to log each pair manually using function calls. A few lines of json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
{
	"sessionId" : "e26380784eff322fd6cb7fb2d145d404-2023.06.24-15.32.43",
	"userId" : "e26380784eff322fd6cb7fb2d145d404",
	"events" : [
		{
			"eventName" : "Build"
,			"attributes" : [
			{
				"name" : "Version",
				"value" : "++UE5+Release-5.2-CL-25360045"
			}
			,
			{
				"name" : "PlatformName",
				"value" : "Windows"
			}
			,
			{
				"name" : "PlatformUserName",
				"value" : "nicholas477"
			}
			,
			{
				"name" : "Config",
				"value" : "Development"
			}
			]
		}
,
		{
			"eventName" : "UserSettings"
,			"attributes" : [
			{
				"name" : "OverallScalability",
				"value" : "Epic"
			}
			,
			{
				"name" : "DesktopResolution",
				"value" : "X=2560 Y=1440"
			}
			]
		}
	]
}

turns into this spaghetti nightmare:

It’s an odd design choice for an engine that has serialization built in. So I decided to use that built in serialization and write my own analytics system that serializes objects to json and sends it wherever. But I noticed that while Unreal has support for UStruct serialization to json, it does not have support for UObjects. So I wrote a plugin for that.

Since the property to json serialization is already included in the engine, the object serialization code itself is pretty short, about 100 lines total.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#include "JsonObjectConverter.h"
#include "UObject/UnrealType.h"

template<typename T>
static TArray<TSharedPtr<FJsonValue>> SerializePropertyAsJsonArray(const T* Data, FArrayProperty* Property, TSet<const UObject*>& TraversedObjects)
{
	const uint8* PropData = Property->ContainerPtrToValuePtr<uint8>(Data);
	FScriptArrayHelper Helper(Property, PropData);
	TArray<TSharedPtr<FJsonValue>> ValueArray;

	for (int32 i = 0, n = Helper.Num(); i < n; ++i)
	{
		const uint8* InnerPropData = Helper.GetRawPtr(i);
		if (FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Property->Inner)) // Array
		{
			TArray<TSharedPtr<FJsonValue>> InnerArray = SerializePropertyAsJsonArray(InnerPropData, ArrayProperty, TraversedObjects);
			ValueArray.Emplace(new FJsonValueArray(InnerArray));
		}
		else if (FStructProperty* StructProperty = CastField<FStructProperty>(Property->Inner)) // Struct
		{
			TSharedPtr<FJsonObject> StructObject = MakeShareable(new FJsonObject);
			const uint8* StructPropData = StructProperty->ContainerPtrToValuePtr<uint8>(InnerPropData);
			for (TFieldIterator<FProperty> PropertyItr(StructProperty->Struct); PropertyItr; ++PropertyItr)
			{
				SerializePropertyAsJsonObjectField((void*)StructPropData, StructObject, *PropertyItr, TraversedObjects);
			}
			ValueArray.Emplace(new FJsonValueObject(StructObject));
		}
		else if (FObjectProperty* ObjectProperty = CastField<FObjectProperty>(Property->Inner)) // Object
		{
			const UObject* SubObject = ObjectProperty->GetObjectPropertyValue_InContainer(InnerPropData);
			if (SubObject->IsValidLowLevel() && !TraversedObjects.Contains(SubObject))
			{
				TraversedObjects.Add(SubObject);
				TSharedPtr<FJsonObject> JsonSubObject = MakeShared<FJsonObject>();
				for (TFieldIterator<FProperty> PropertyItr(SubObject->GetClass()); PropertyItr; ++PropertyItr)
				{
					SerializePropertyAsJsonObjectField(SubObject, JsonSubObject, *PropertyItr, TraversedObjects);
				}
				ValueArray.Emplace(new FJsonValueObject(JsonSubObject));
			}
		}
		else
		{
			TSharedPtr<FJsonValue> JsonValue;
			const uint8* InnerInnerPropData = Property->Inner->ContainerPtrToValuePtr<uint8>(InnerPropData);
			ValueArray.Emplace(FJsonObjectConverter::UPropertyToJsonValue(Property->Inner, InnerInnerPropData));
		}
	}
	return ValueArray;
}

template<typename T>
static void SerializePropertyAsJsonObjectField(const T* Data, TSharedPtr<FJsonObject> OuterObject, FProperty* Property, TSet<const UObject*>& TraversedObjects)
{
	if (Property->GetName() == "UberGraphFrame"
		|| Property->HasAnyPropertyFlags(CPF_Transient))
	{
		// Don't include "UberGraphFrame" or any transient properties
		return;
	}

	if (FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Property)) // Array
	{
		TArray<TSharedPtr<FJsonValue>> Values = SerializePropertyAsJsonArray(Data, ArrayProperty, TraversedObjects);
		OuterObject->SetArrayField(Property->GetAuthoredName(), Values);
	}
	else if (FStructProperty* StructProperty = CastField<FStructProperty>(Property)) // Struct
	{
		TSharedRef<FJsonObject> StructObject = MakeShareable(new FJsonObject);
		const uint8* PropData = Property->ContainerPtrToValuePtr<uint8>(Data);
		for (TFieldIterator<FProperty> PropertyItr(StructProperty->Struct); PropertyItr; ++PropertyItr)
		{
			SerializePropertyAsJsonObjectField((void*)PropData, StructObject, *PropertyItr, TraversedObjects);
		}
		OuterObject->SetObjectField(Property->GetAuthoredName(), StructObject.ToSharedPtr());
	}
	else if (FObjectProperty* ObjectProperty = CastField<FObjectProperty>(Property)) // Object
	{
		const UObject* SubObject = ObjectProperty->GetObjectPropertyValue_InContainer(Data);
		if (SubObject->IsValidLowLevel() && !TraversedObjects.Contains(SubObject))
		{
			TraversedObjects.Add(SubObject);
			TSharedPtr<FJsonObject> JsonSubObject = MakeShared<FJsonObject>();
			for (TFieldIterator<FProperty> PropertyItr(SubObject->GetClass()); PropertyItr; ++PropertyItr)
			{
				SerializePropertyAsJsonObjectField(SubObject, JsonSubObject, *PropertyItr, TraversedObjects);
			}
			OuterObject->SetObjectField(Property->GetAuthoredName(), JsonSubObject);
		}
	}
	else
	{
		TSharedPtr<FJsonValue> JsonValue;
		const uint8* PropData = Property->ContainerPtrToValuePtr<uint8>(Data);
		OuterObject->SetField(Property->GetAuthoredName(), FJsonObjectConverter::UPropertyToJsonValue(Property, PropData));
	}
}

TSharedPtr<FJsonObject> FJsonSerializationModule::SerializeUObjectToJson(const UObject* Object)
{
	TSet<const UObject*> TraversedObjects;
	TraversedObjects.Add(Object);

	TSharedPtr<FJsonObject> JsonObject = MakeShared<FJsonObject>();

	for (TFieldIterator<FProperty> PropertyItr(Object->GetClass()); PropertyItr; ++PropertyItr)
	{
		SerializePropertyAsJsonObjectField(Object, JsonObject, *PropertyItr, TraversedObjects);
	}

	return JsonObject;
}

I thought this would be incredibly useful for anyone else who wants to avoid the hassle of the event logging system, so I put the code up on github as a plugin. Grab it below.

Download Links