ufna / VaRest

REST API plugin for Unreal Engine 4 - we love restfull backend and JSON communications!
https://www.unrealengine.com/marketplace/en-US/product/varest-plugin
MIT License
1.07k stars 292 forks source link

Add wildcard struct <--> string serialization #333

Open angelhodar opened 3 years ago

angelhodar commented 3 years ago

Hey @ufna, I just wanted to know if it would be possible to create a generic node that accepts a wildcard struct and returns the decoded json string infering the struct variables (and the reverse decode node), is the #234 done for this? It would be extremely helpful as it would provide instant mapping between network data and engine struct data. There is a plugin in the marketplace that do this already, but costs 25$.

I have thought that the source code from the DataTable export to JSON functionality (lines 109-400) could be really helpful as it works with all types of data (even nested structs, TMap, etc).

Maxmystere commented 3 years ago

If you don't mind using UObject you should be able to use this

UVaRestJsonObject* UJsonObjectConverter::ObjectToJson(UObject* ObjectToConvertToJson)
{
    TSharedPtr<FJsonObject> JsonObject = MakeShareable(new FJsonObject());

    for (TFieldIterator<FProperty> PropIt(ObjectToConvertToJson->GetClass()); PropIt; ++PropIt)
    {
        FProperty* Property = *PropIt;
        void* ValuePtr = Property->ContainerPtrToValuePtr<void>(ObjectToConvertToJson);

        JsonObject->SetField(Property->GetName(), FJsonObjectConverter::UPropertyToJsonValue(Property, ValuePtr, 0, 0));
    }

    UVaRestJsonObject* VaRestJson = NewObject<UVaRestJsonObject>(ObjectToConvertToJson);
    VaRestJson->SetRootObject(JsonObject);

    return (VaRestJson);
}
void UJsonObjectConverter::JsonToObject(const UVaRestJsonObject* VaRestJson, UObject* ObjectToPopulateFromJson)
{
    if (!IsValid(VaRestJson))
    {
//      UE_LOG(LogTemp, Error, TEXT("%s: Invalid Json"), *VA_FUNC_LINE);
        return;
    }
    for (TFieldIterator<FProperty> PropIt(ObjectToPopulateFromJson->GetClass()); PropIt; ++PropIt)
    {
        FProperty* Property = *PropIt;
        void* ValuePtr = Property->ContainerPtrToValuePtr<void>(ObjectToPopulateFromJson);

        if (VaRestJson->HasField(Property->GetName()))
        {
            FJsonObjectConverter::JsonValueToUProperty(
                VaRestJson->GetField(Property->GetName())->GetRootValue(),
                Property, ValuePtr, 0, 0);
        }
    }
}
angelhodar commented 3 years ago

Hi @Maxmystere!! Thank you you taking your time. It seems a pretty elegant solution, and as UStruct is subclass of UObject and its properties are iterable it should not give any problem, in fact it would allow Actors to be used too! The thing is that I am not sure if your code supports nested structs, TMaps, and so on, does it? Also, would this work as simple wildcard blueprint node? I have seen that in order to do this we would need to use the DECLARE_FUNCTION macro getting the properties from the stack. I have been talking with a guy from Unreal Slackers and he seems to have found the solution, but he will not be available until tomorrow. He shows a function that looks like this:

UFUNCTION(BlueprintCallable, Category = "Json", CustomThunk, meta = (CustomStructureParam = "StructToFill"))
static bool JsonObjectStringToStruct(const FString& JsonString, const UStruct* StructToFill);

DECLARE_FUNCTION(execJsonObjectStringToStruct)
    {
        // Grab the reference to the Json string inside the Thunk. This advances one step on the Stack
        P_GET_PROPERTY_REF(FStrProperty, JsonString);

        // Grab both Struct definition and type from the second param StructToFill. ORDER OF OPERATIONS IS KEY here.
        // This works only because of the previous call to P_GET_PROPERTY_REF which left the stack on top of the struct
        Stack.StepCompiledIn<FStructProperty>(NULL);
        void* StructPtr                 = Stack.MostRecentPropertyAddress;
        FStructProperty* StructProperty = CastField<FStructProperty>(Stack.MostRecentProperty);

        // End the process of reading the params
        P_FINISH;

        UStruct* StructDefinition = StructProperty->Struct;
        bool bSuccess             = true;

        // First deserialize the Json string into a Json Object
        TSharedPtr<FJsonObject> JsonObject;
        TSharedRef<TJsonReader<>> JsonReader = TJsonReaderFactory<>::Create(JsonString);
        if(!FJsonSerializer::Deserialize(JsonReader, JsonObject) || !JsonObject.IsValid())
        {
            UE_LOG(LogJson, Warning, TEXT("JsonObjectStringToUStruct - Unable to parse json=[%s]"), *JsonString);
            bSuccess = false;
        }

        // Now turn the Json Object into a UStruct of the same type as the second argument
        if(!FJsonObjectConverter::JsonObjectToUStruct(JsonObject.ToSharedRef(), StructDefinition, StructPtr, 0, 0))
        {
            UE_LOG(LogJson, Warning, TEXT("JsonObjectStringToUStruct - Unable to deserialize. json=[%s]"), *JsonString);
            bSuccess = false;
        }

        *static_cast<bool*>(RESULT_PARAM) = bSuccess;
    }

The way to make wildcard bp nodes is by using the CustomThunk attr, I saw how here in case you have doubts). I have taken a deeper look at the DataTable JSON parsing functionality and they seem to do it by hand with the JsonWriter/JsonReader classes, does it makes any sense? I say it because yours solution and Nahuel's (discord guy) one seems to be more elegant using the FJsonObjectConverter