Deserializer

In the previous chapter we mentioned that DataConfig data model is a super set of the property system. FDcDeserializer is used to convert different subsets of it into the property system.

Context

A company class to the deserializer is FDcDeserializeContext:

// DataConfig/Source/DataConfigCore/Public/DataConfig/Deserialize/DcDeserializeTypes.h
struct DATACONFIGCORE_API FDcDeserializeContext
{
    //...
    FDcDeserializer* Deserializer;
    FDcReader* Reader;
    FDcPropertyWriter* Writer;
    //...
};

Comparing to FDcPipeVisitor which takes a FDcReader and a FDcWriter, FDcDeserializeContext takes explicitly a FDcPropertyWriter to construct. The deserializer reads from arbitrary reader but writes only into the property system objects.

Handlers

Custom deserialize logic is provided through FDcDeserializeDelegate:

// DataConfig/Source/DataConfigCore/Public/DataConfig/Deserialize/DcDeserializeTypes.h
using FDcDeserializeDelegateSignature = FDcResult(*)(FDcDeserializeContext& Ctx);
DECLARE_DELEGATE_RetVal_OneParam(FDcResult, FDcDeserializeDelegate, FDcDeserializeContext&);

We call these functions handlers. Here's a simple one that deserialize booleans:

// DataConfig/Source/DataConfigCore/Private/DataConfig/Deserialize/Handlers/Json/DcJsonPrimitiveDeserializers.cpp
FDcResult HandlerBoolDeserialize(FDcDeserializeContext& Ctx)
{
    EDcDataEntry Next;
    DC_TRY(Ctx.Reader->PeekRead(&Next));

    if (Next != EDcDataEntry::Bool)
    {
        return DC_FAIL(DcDDeserialize, DataEntryMismatch)
            << EDcDataEntry::Bool << Next;
    }

    bool Value;
    DC_TRY(Ctx.Reader->ReadBool(&Value));
    DC_TRY(Ctx.Writer->WriteBool(Value));

    return DcOk();
}

Note how we propagate errors to the caller by using DC_TRY or fail explicitly by returning DC_FAILwith diagnostic.

Predicates

In many occasions we want to provide custom deserialization logic for a very specific class. The selection process is done through FDcDeserializePredicate:

// DataConfig/Source/DataConfigCore/Public/DataConfig/Deserialize/DcDeserializeTypes.h
using FDcDeserializePredicateSignature = EDcDeserializePredicateResult(*)(FDcDeserializeContext& Ctx);
DECLARE_DELEGATE_RetVal_OneParam(EDcDeserializePredicateResult, FDcDeserializePredicate, FDcDeserializeContext&);

We call these Predicates. Here's an example of selecting the bulit-in FColor:

// DataConfig/Source/DataConfigExtra/Private/DataConfig/Extra/Deserialize/DcDeserializeColor.cpp
EDcDeserializePredicateResult PredicateIsColorStruct(FDcDeserializeContext& Ctx)
{
    UScriptStruct* Struct = DcPropertyUtils::TryGetStructClass(Ctx.TopProperty());
    return Struct && Struct == TBaseStructure<FColor>::Get()
        ? EDcDeserializePredicateResult::Process
        : EDcDeserializePredicateResult::Pass;
}

Similar to handlers it's checking Ctx.TopProperty() and return a EDcDeserializePredicateResult to decide to process or pass.

Deserializer

Finally there's the FDcDeserializer which is just a collection of predicates and handlers. It contains no mutable state as those are put in FDcDeserializeContext:

// DataConfig/Source/DataConfigCore/Public/DataConfig/Deserialize/DcDeserializer.h
struct DATACONFIGCORE_API FDcDeserializer : public FNoncopyable
{
    //...
    FDcResult Deserialize(FDcDeserializeContext& Ctx);

    void AddDirectHandler(FFieldClass* PropertyClass, FDcDeserializeDelegate&& Delegate);
    void AddDirectHandler(UClass* PropertyClass, FDcDeserializeDelegate&& Delegate);
    void AddPredicatedHandler(FDcDeserializePredicate&& Predicate, FDcDeserializeDelegate&& Delegate);
    //...
};

AddDirectHandler() registers handlers for a specific property type. Here's an example of registering the HandlerBoolDeserialize above:

// DataConfig/Source/DataConfigCore/Private/DataConfig/Deserialize/DcDeserializerSetup.cpp
Deserializer.AddDirectHandler(
    FBoolProperty::StaticClass(), 
    FDcDeserializeDelegate::CreateStatic(HandlerBoolDeserialize)
);

AddPredicatedHandler() registers a predicate and handler pair. Here's an example of registering the PredicateIsColorStruct predicate above:

// DataConfig/Source/DataConfigTests/Private/DcTestBlurb.cpp
Deserializer.AddPredicatedHandler(
    FDcDeserializePredicate::CreateStatic(DcExtra::PredicateIsColorStruct),
    FDcDeserializeDelegate::CreateStatic(DcExtra::HandlerColorDeserialize)
);

To start deserialization you need to prepare a FDcDeserializeContext and call FDcDeserializer::Deserialize(Ctx):

// DataConfig/Source/DataConfigTests/Private/DcTestBlurb.cpp
//  prepare context for this run
FDcPropertyDatum Datum(FDcTestExampleStruct::StaticStruct(), &Dest);
FDcJsonReader Reader(Str);
FDcPropertyWriter Writer(Datum);

FDcDeserializeContext Ctx;
Ctx.Reader = &Reader;
Ctx.Writer = &Writer;
Ctx.Deserializer = &Deserializer;
Ctx.Properties.Push(Datum.Property);
DC_TRY(Ctx.Prepare());

//  kick off deserialization
DC_TRY(Deserializer.Deserialize(Ctx));

Tips for writing handlers

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

Recursive Deserialize

When deserializing a container like USTRUCT root or TArray you'll need to recursively deserialize children properties. Here's how it's done:

// DataConfig/Source/DataConfigCore/Private/DataConfig/Deserialize/Handlers/Json/DcJsonStructDeserializers.cpp
FDcScopedProperty ScopedValueProperty(Ctx);
DC_TRY(ScopedValueProperty.PushProperty());
DC_TRY(Ctx.Deserializer->Deserialize(Ctx));

FDcScopedProperty is used to push writer's next property into FDcDeserializeContext::Properties to satisfiy the invariant that FDcDeserializeContext::TopProperty() always points to the current writing property.

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():

// DataConfig/Source/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:

// DataConfig/Source/DataConfigEditorExtra/Private/DataConfig/EditorExtra/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:

// DataConfig/Source/DataConfigEditorExtra/Private/DataConfig/EditorExtra/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.