JsonObjectConverter removes the "_ClassName" property from an in memory JSON file during conversion

Hello,

I have JSON files stored in memory via FJsonObject that I am using to convert to UObjects as required at runtime. This works totally fine the first time but on and after the second attempt the conversion fails. This is because the code shown below removes the ObjectClassNameKey from the JSON object at runtime, I assume this is done to avoid an error downstream when converting the JSON to a UObject or something like that? Couldn’t this be fixed by simply ignoring this field instead?

It becomes painful if you want to create a new UObject from the same in memory JSON file you can’t because now that in memory JSON file is not longer compatible for deserialisation and the only options you are left with are to re-create the in memory JSON file or to duplicate the UObject after you’ve created the first instance, neither of which are ideal solutions when I have what should be a perfectly good JSON object in memory already.

Could you let me know if this is something that you would consider changing?

Cheers!

// If a specific subclass was stored in the JSON, use that instead of the PropertyClass
FString ClassString = Obj->GetStringField(ObjectClassNameKey);
Obj->RemoveField(ObjectClassNameKey);
if (!ClassString.IsEmpty())
{
    UClass* FoundClass = FPackageName::IsShortPackageName(ClassString) ? FindFirstObject<UClass>(*ClassString) : LoadClass<UObject>(nullptr, *ClassString);
    if (FoundClass)
    {
       PropertyClass = FoundClass;
    }
}

[Attachment Removed]

Steps to Reproduce

  1. Try to convert any FJsonObject which contains a _ClassName field to a UObject more than once.
    [Attachment Removed]

Hello [mention removed]​,

Thanks for reporting this. I confirmed that the _ClassName field is removed from the in-memory FJsonObject during conversion. However, in my tests, converting the same in-memory JSON object a second time still returned success. I tested this locally in UE 5.6, 5.7, and also in a recent UE5-Main source build.

FJsonObjectConverter seems to use the property’s declared class by default, and _ClassName is only used when it needs to switch to a more specific class from the JSON. Because of that, even after _ClassName is removed, the conversion can still succeed if the declared property type is already enough.

In my testing, I used FJsonObjectConverter::JsonObjectToUStruct(…) with a wrapper struct containing an instanced UObject property.

TSharedPtr<FJsonObject> InnerObject = MakeShared<FJsonObject>();
    InnerObject->SetStringField(TEXT("_ClassName"), TEXT("/Script/JSONCONVERTCASE.MyInnerObject"));
    InnerObject->SetNumberField(TEXT("Value"), 777);
 
    TSharedPtr<FJsonObject> PayloadObject = MakeShared<FJsonObject>();
    PayloadObject->SetStringField(TEXT("_ClassName"), TEXT("/Script/JSONCONVERTCASE.MyTestObject"));
    PayloadObject->SetNumberField(TEXT("Number"), 123);
    PayloadObject->SetStringField(TEXT("Label"), TEXT("First Object"));
    PayloadObject->SetObjectField(TEXT("Inner"), InnerObject);
 
    TSharedPtr<FJsonObject> RootObject = MakeShared<FJsonObject>();
    RootObject->SetObjectField(TEXT("Payload"), PayloadObject);
 
    FTestJsonWrapper FirstResult;
    const bool bSuccess = FJsonObjectConverter::JsonObjectToUStruct(
        RootObject.ToSharedRef(),
        FTestJsonWrapper::StaticStruct(),
        &FirstResult,
        0,
        0);

In that setup, the second conversion still returned success and deserialization completed.

Could you share what type of UObject/UStruct you’re converting into when the issue occurs and confirm if you’re using FJsonObjectConverter::JsonObjectToUStruct(…)? If possible, a small repro project or minimal code example would also help me try to reproduce the same behavior on my end.

Best,

Francisco

[Attachment Removed]

Hey,

I’ve prepared a simple repro which I have confirmed to highlight the issue, the code is awful I know, I just did it very quickly to demo this issue.

Header Code

UCLASS(Abstract, DefaultToInstanced)
class UJsonExampleBase : public UObject
{
	GENERATED_BODY()
 
public:
 
	UPROPERTY()
	int32 TestInt = 8194;
};
 
UCLASS()
class UJsonExampleDerived : public UJsonExampleBase
{
	GENERATED_BODY()
 
public:
 
	UPROPERTY()
	bool bDerived = true;
};
 
UCLASS()
class UJsonExampleExport : public UObject
{
	GENERATED_BODY()
 
public:
 
	UPROPERTY(Instanced)
	TArray<UJsonExampleBase*> Objects;
};

CCP Code

UJsonExampleExport* ObjectToExport = NewObject<UJsonExampleExport>();
 
ObjectToExport->Objects.Add(NewObject<UJsonExampleDerived>());
 
TSharedPtr<FJsonObject> JsonObject = MakeShared<FJsonObject>();
 
FJsonObjectConverter::UStructToJsonObject(UJsonExampleExport::StaticClass(), ObjectToExport, JsonObject.ToSharedRef());
 
UJsonExampleExport* ObjectToImportInto1 = NewObject<UJsonExampleExport>();
FJsonObjectConverter::JsonObjectToUStruct(JsonObject.ToSharedRef(), ObjectToImportInto1);
	
UJsonExampleExport* ObjectToImportInto2 = NewObject<UJsonExampleExport>();
FJsonObjectConverter::JsonObjectToUStruct(JsonObject.ToSharedRef(), ObjectToImportInto2);

When running with the debugger attached you should get an ensure trigger on the second call to JsonObjectToUStruct saying that it cannot instantiate an abstract class, this is because the _ClassNamefield has been removed during the first call to JsonObjectToUStruct so that FJsonObject no longer has enough information inside of it to properly re-create the UObject that it was exported from.

The whole problem here is that JsonObjectToUStruct takes a non-const parameter for the JSON value, it takes const TSharedRef<FJsonObject>& JsonObject when it should be const TSharedRef<const FJsonObject>& JsonObject because having the source object of a type conversion be mutable just feels very wrong and it leads to issues like this.

The only work arounds I can think of right now are to duplicate the FJsonObject, modify the Engine in some way to not remove the _ClassName field or to manually fix up the _ClassName fields that get removed, none of those options are particularly great though as I think I would have to add a new parameter to all associated functions to make the Engine mod work which, I may end up doing.

Hopefully that is enough information, please let me know if not though.

[Attachment Removed]

Hello [mention removed]​,

Thanks for the detailed response. I was able to reproduce the behavior on my end in UE 5.7 and a recent source build from UE5-Main.

Reviewing the engine code, this appears to be related to the removal of _ClassName via Obj->RemoveField(ObjectClassNameKey) in ConvertScalarJsonValueToFPropertyWithContainer. I’ll go ahead and file a report with the engine team so they can review this issue and determine the appropiate fix.

As a temporary workaround, you may try commenting out the removal of _ClassName. In my local testing, that avoided the issue.

I’ll follow up here once I have more information to share.

Best,

Francisco

[Attachment Removed]

Hello again [mention removed]​,

I’ve filed a report for this issue so the engine team can review it and determine the appropriate fix. You will be able to track its status at the following link: https://issues.unrealengine.com/issue/UE\-370446\.

Please note it may take some time before the issue becomes publicly visible.

In the meantime, as a temporary workaround, you may try commenting out the removal of _ClassName in ConvertScalarJsonValueToFPropertyWithContainer. In my local testing on UE5-Main CL 51575875, that avoided the issue without introducing additional problems.

Please let me know if you need anything else regarding this case. Otherwise, I’ll go ahead and close this ticket.

Best,

Francisco

[Attachment Removed]