Tips for writing handlers

There're some recurring patterns when writing handlers in DataConfig.

Recursive Deserialize

When deserializing a container like USTRUCT root or TArray you'll need to recursively deserialize children properties. This is wrapped in a single function call:

// DataConfigCore/Private/DataConfig/Deserialize/Handlers/Json/DcJsonStructDeserializers.cpp
DC_TRY(DcDeserializeUtils::RecursiveDeserialize(Ctx));

Internally it would push writer's next property into FDcDeserializeContext::Properties to satisfiy the invariant that FDcDeserializeContext::TopProperty() always points to the current writing property. It would also clear up the top property on return.

Another example is how we pipe deserialize a TMap<>. When at key and value position we simply call this method 2 times:

// DataConfigCore/Private/DataConfig/Deserialize/Handlers/Property/DcPropertyPipeDeserializers.cpp
DC_TRY(Ctx.Reader->ReadMapRoot());
DC_TRY(Ctx.Writer->WriteMapRoot());

EDcDataEntry CurPeek;
while (true)
{
    DC_TRY(Ctx.Reader->PeekRead(&CurPeek));
    if (CurPeek == EDcDataEntry::MapEnd)
        break;

    DC_TRY(DcDeserializeUtils::RecursiveDeserialize(Ctx));
    DC_TRY(DcDeserializeUtils::RecursiveDeserialize(Ctx));
}

DC_TRY(Ctx.Reader->ReadMapEnd());
DC_TRY(Ctx.Writer->WriteMapEnd());

Provide TopObject()

Sometimes deserialization will create new UObject along the way. In this case you'll need to fill in FDcDeserializeContext::Objects so the top one is used for NewObject() calls. For transient objecst you can use GetTransientPackage():

// DataConfigTests/Private/DcTestDeserialize.cpp
Ctx.Objects.Push(GetTransientPackage());

Peek By Value

Sometimes you want to peek the content of the next entry. For example in DcExtra::HandlerBPDcAnyStructDeserialize() we're dealing with a JSON like this:

// DataConfigEngineExtra/Private/DataConfig/EngineExtra/Deserialize/DcDeserializeBPClass.cpp
{
    "AnyStructField1" : {
        "$type" : "/DataConfig/DcFixture/DcTestBlueprintStructWithColor",
        "NameField" : "Foo",
        //...
    }
}

We want to consume the $type key and its value, and then delegate the logic back to the deserializer. The solution here is first to consume the pair. Then we put back a { then replace the reader:

// DataConfigEngineExtra/Private/DataConfig/EngineExtra/Deserialize/DcDeserializeBPClass.cpp
FDcPutbackReader PutbackReader(Ctx.Reader);
PutbackReader.Putback(EDcDataEntry::MapRoot);
TDcStoreThenReset<FDcReader*> RestoreReader(Ctx.Reader, &PutbackReader);

FDcScopedProperty ScopedValueProperty(Ctx);
DC_TRY(ScopedValueProperty.PushProperty());
DC_TRY(Ctx.Deserializer->Deserialize(Ctx));

Beware that Putback only support a limited subset of data types.

Coercion

Readers implements FDcReader::Coercion() which can be used to query if the next value can be coerced into other types.

Here's an example of reading a JSON number as string:

// DataConfigTests/Private/DcTestBlurb2.cpp
FDcJsonReader Reader(TEXT(R"(
    1.234567
)"));

//  check coercion
//  note this is a query and doesn't affect reading at all
bool bCanCoerceToStr;
DC_TRY(Reader.Coercion(EDcDataEntry::String, &bCanCoerceToStr));
check(bCanCoerceToStr);

//  read number token as stjring
//  note here we skipped parsing the number to float
FString NumStr;
DC_TRY(Reader.ReadString(&NumStr));

check(NumStr == TEXT("1.234567"));
return DcOk();

What's cool here is that FDcJsonReader does parsing at the ReadFloat() callsite. So when doing coercion above it actually skipped string to float parsing.

Here's a table of all existing coercion rules:

ReaderFrom TypeTo Type
FDcPropertyReaderArrayBlob
StructRootBlob
FDcJsonReaderDoubleString
DoubleInt8/Int16/Int32/Int64
UInt8/Uint16/UInt32/Uint64
Float
StringName/Text
FDcMsgPackReaderStringName/Text

Some caveats regarding coercion:

  • When reading from JSON/MsgPack string can be read as Name/Text for convenient.
  • When reading from Property, Array/Struct can be read as a FDcBlobViewData which directly points to the memory span.