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.