Assert in async loading code at editor startup

We have some custom code that creates UObject instances from JSON using FJsonObjectConverter::JsonObjectToUStruct. This happens when the asset registry has finished loading files.

IAssetRegistry& assetRegistry = IAssetRegistry::GetChecked();
if (assetRegistry.IsLoadingAssets())
{
    assetRegistry.OnFilesLoaded().AddRaw(this, &FCustomModule::Initialize);
}

This works fine in Unreal 5.4. We are upgrading to 5.6 and getting a lot of asserts when it initializes, specifically on this line:

void UObject::ConditionalPostLoad()
{
    ...
    ensureAlwaysMsgf((GetLoaderType() != ELoaderType::ZenLoader) || !HasAnyFlags(RF_NeedLoad), TEXT("Object '%s' does not have RF_NeedLoad cleared in PostLoad!"), *GetFullName());

The assert isn’t happening on the object being loaded by the JSON. Rather, it seems the issue is this:

  • Calling StaticLoadObject is triggering FlushAsyncLoading(…)
  • That flush triggers a Blueprint compilation queue flush
  • It tries to duplicate an object that has the RF_NeedLoad flag still
  • Asserts when it tries to call PostLoad on that object

Any ideas on how we can fix this or work around it?

Here is the compressed callstack (too many characters for the full one):

`UObject::ConditionalPostLoad'::`2'::<lambda_1>::operator(...)
UObject::ConditionalPostLoad()
StaticDuplicateObjectEx(...)
StaticDuplicateObject(...)
FBlueprintCompileReinstancer::MoveCDOToNewClass(...)
FBlueprintCompileReinstancer::FBlueprintCompileReinstancer(...)
FBlueprintCompilationManagerImpl::FlushCompilationQueueImpl(...)
FBlueprintCompilationManager::FlushCompilationQueue(...)
FScopedClassDependencyGather::~FScopedClassDependencyGather()
FLinkerLoad::CreateExport(...)
FLinkerLoad::IndexToObject(...)
FLinkerLoad::CreateExport(...)
FLinkerLoad::IndexToObject(...)
FLinkerLoad::CreateExport(...)
FAsyncPackage2::CreateLinkerLoadExports(...)
FAsyncPackage2::Event_CreateLinkerLoadExports(...)
FEventLoadNode2::Execute(...)
FAsyncLoadEventQueue2::ExecuteSyncLoadEvents(...)
FAsyncLoadingThread2::ProcessAsyncLoadingFromGameThread(...)
[Inlined] FAsyncLoadingThread2::TickAsyncThreadFromGameThread(...)
FAsyncLoadingThread2::TickAsyncLoadingFromGameThread(...)
FAsyncLoadingThread2::FlushLoading(...)
FlushAsyncLoading(...)
FlushAsyncLoading(...)
LoadPackageInternal(...)
LoadPackage(...)
LoadPackage(...)
ResolveName2(...)
StaticLoadObjectInternal(...)
StaticLoadObject(...)
FObjectPropertyBase::FindImportedObject(...)
[Inlined] IsObjectHandleNull(...)
[Inlined] FObjectPtr::operator bool()
[Inlined] ObjectPtr_Private::IsObjectPtrNull(...)
[Inlined] TObjectPtr::operator==(...)
FObjectPropertyBase::ParseObjectPropertyValue(...)
FObjectPropertyBase::ImportText_Internal(...)
FObjectProperty::ImportText_Internal(...)
FProperty::ImportText_Direct(...)
`anonymous namespace'::ConvertScalarJsonValueToFPropertyWithContainer(...)
`anonymous namespace'::JsonValueToFPropertyWithContainer(...)
`anonymous namespace'::JsonAttributesToUStructWithContainer(...)
`anonymous namespace'::ConvertScalarJsonValueToFPropertyWithContainer(...)
`anonymous namespace'::JsonValueToFPropertyWithContainer(...)
`anonymous namespace'::ConvertScalarJsonValueToFPropertyWithContainer(...)
`anonymous namespace'::JsonValueToFPropertyWithContainer(...)
`anonymous namespace'::JsonAttributesToUStructWithContainer(...)
[Inlined] FJsonObjectConverter::JsonAttributesToUStruct(...)
FJsonObjectConverter::JsonObjectToUStruct(...)
FJsonDataObjectConverter::JsonObjectToStruct(...)
FJsonDataObjectConverter::JsonObjectToUObject(...)
FJsonDataObjectReader::SerializePropertyValues(...)
FDataObjectReader::SerializeObjectData(...)
UDataObject::Create(...)
FCustomModule::Initialize()
TBaseRawMethodDelegateInstance::ExecuteIfSafe()
[Inlined] TMulticastDelegateBase::Broadcast()
TMulticastDelegate::Broadcast() DelegateSignatureImpl.inl:1080
UAssetRegistryImpl::Broadcast(...)
UAssetRegistryImpl::Tick(...)
[Inlined] FAssetRegistryModule::TickAssetRegistry(...)
UEditorEngine::Tick(...)
UUnrealEdEngine::Tick(...)
FEngineLoop::Tick()
[Inlined] EngineTick()
GuardedMain(...)
LaunchWindowsStartup(...)
WinMain(...)

Steps to Reproduce

Hi John,

This works fine in 5.4 because, the new zen loader, while available, wasn’t enabled by default in that version and what still experimental. One additional validation we added when using the new loader is to make sure that an object has properly been deserialized prior to being postloaded. There were often silent issues that happened in certain cases with the old loader where objects were used and post loaded prior to even being properly deserialized causing harder to track issues later…

With the zen loader a sync load such as StaticLoadObject will result in going through an async flush but only of that single request (not a full flush of the queue). That being said when Blueprint compilation request then needs to be flushed however, they might not end up fulfilling the a specific request tied to the package being flushed at this time. That’s not usually a problem but it would be good to know if this is the case you are hitting here. Is the object being duplicated part of the request that is currently being flushed or something relating to another package entirely? The BP compiler should already making sure to force the deserialization of objects that are yet to be deserialized before attempting to duplicate them which seems not to be the case here. This might be a bug in the compiler that needs fixing. If you have logs or any other information that could help us to track this down it would be appreciated.

As for working around the issue at this time, you can either go back to the old loader by adding `-nozenloader` to your commandline or you could temporarily comment this additional assert. However, it would probably be better to help us track down the issue to make sure objects aren’t being used prematurely.

Thanks,

Francis

Hi Francis, thank you for the detailed response! I’d like to add a bit more detail to what John posted in case it helps shine a light on the issue further.

In our case, all of the ensures fired on one of two subobject types: a MovieScene or a CurveFloat

And every one of these had an outer with a path starting with “/Engine/Transient.REINST_”

Majority of them being Widget BPs or BPs with widget components

We tried this workaround at the top of UObject::ConditionalPostLoad() which got rid of the ensures, but feels too hacky for my taste:

	if (HasAllFlags(RF_NeedLoad | RF_WasLoaded))
	{
		ClearFlags(RF_NeedLoad);
	}

Let me know if there is any further info that we could provide to help

Thank you

Hey Eugene,

Sorry for the delay in my answer. This seems that it might be a Widget BP specific problem of some kind, that might have been lurking for a while but that because the old doesn’t validate that things are actually properly loaded, you were not hitting the problem in question. In this case, the code is duplicating class X to class REINST_X for reinstantiation purpose and class X isn’t deserialized yet. The old loader let’s that happen silently but that seems like a bug. if it turns out that way it might be something particular with circular dependencies between those widget BP. Obviously you can indeed silence the issue and ignore the potential bug, but if this is the approach you want to take then I would suggest just commenting the `ensureAlwaysMsgf` that’s part of the ConditionalPostLoad. I would suggest bumping verbosity of LogStreaming to VeryVerbose and sharing a log however, maybe we can find why we end up with object not yet loaded while trying to reinstantiate data post compilation.

Thank you!

Francis