How to properly deprecate data?

Hi Support friends,

What would be the prescribed method to deprecate a member of a UStruct in favor of something of another type but semantically related?

(the details are in the the repro steps section)

Thanks,

D

Steps to Reproduce
Hi Support fellows,

Say I have the following struct:

What would be the proper way to deprecate FStatementData::IsTruthful to replace it by FStatementData::Truthfulness (type=ETruthfulness)?

USTRUCT()
struct FStatementData
{
	GENERATED_BODY()
	UPROPERTY()
	FString Description;

	UPROPERTY()
	bool IsTruthful;
};

Now we’re introducing the following enum:

UENUM()

enum class ETruthfulness : uint8
{
 False = 0,
 True,
 Undetermined,
 Max META(Hidden)
};
ENUM_RANGE_BY_COUNT(ETruthfulness, ETruthfulness::Max);

What would be the proper way to deprecate FStatementData::IsTruthful to replace it by FStatementData::Truthfulness (type=ETruthfulness)?

Ideally, I would like to be able to read older data (let’s call it V0) containing the original bool flag and automatically assign FStatementData::Truthfulness to eitther ETruthfulness::True or ETruthfulness::False depending on its value.

Thanks,

D

Hi David,

To deprecate a member of a USTRUCT, it should be enough to append suffix “_DEPRECATED” (all caps) to its name. This will also forbid exposing that member with tags such as VisibleAnywhere, EditAnywhere or BlueprintReadWrite. In your example, it would look like this:

USTRUCT()
struct FStatementData
{
	GENERATED_BODY()
 
	UPROPERTY()
	FString Description;
 
	UPROPERTY()
	bool IsTruthful_DEPRECATED;
 
	UPROPERTY()
	ETruthfulness Truthfulness;
};

If you want the C++ to generate a warning when compiling code that attempts to use the deprecated member, you can add macro UE_DEPRECATED() before it. Additionally, if you want to make sure that all systems treat the member as deprecated, you can add the following Meta tags to your UPROPERTY declaration: (DeprecatedProperty, DeprecationMessage = “”). In your example:

USTRUCT()
struct FStatementData
{
	GENERATED_BODY()
 
	UPROPERTY()
	FString Description;
 
	UE_DEPRECATED(MyVersion, "IsTruthful is deprecated, please use Truthfulness instead")
	UPROPERTY(meta = (DeprecatedProperty, DeprecationMessage = "IsTruthful is deprecated, please use Truthfulness instead"))
	bool IsTruthful_DEPRECATED;
 
	UPROPERTY()
	ETruthfulness Truthfulness;
};

Now, to be able to read older data containing the original bool flag, your struct can provide methods Serialize() and/or PostSerialize() to perform backwards compatibility adjustments. These methods will be used by the serialization process if you enable them through type traits for your struct:

USTRUCT()
struct FStatementData
{
	GENERATED_BODY()
 
	(...)
 
	bool Serialize(const FArchive& Ar);
	void PostSerialize(const FArchive& Ar);
 
};
 
template<>
struct TStructOpsTypeTraits<FStatementData> : TStructOpsTypeTraitsBase2<FStatementData>
{
	enum {
		WithSerializer = true,
		WithPostSerialize = true,
	};
};

In general, PostSerialize() should be enough to make simple backwards-compatibility adjustments:

bool FStatementData::Serialize(const FArchive& Ar)
{
	// We can take complete control of our struct's serialization here.
	// If we did serialize it, return true.
	// Otherwise, return false so that the default serialization can take place.
	// Returning false can be useful to do some pre-serialize work here if necessary.
	return false;
}
 
void FStatementData::PostSerialize(const FArchive& Ar)
{
	if (Ar.IsLoading() && Ar.IsPersistent())
	{
			// The following macro disabled C++ compiler deprecation warnings that would otherwise be generated from UE_DEPRECATED()
			PRAGMA_DISABLE_DEPRECATION_WARNINGS
		
			Truthfulness = IsTruthful_DEPRECATED ? ETruthfulness::True : ETruthfulness::False;
		
			PRAGMA_ENABLE_DEPRECATION_WARNINGS
		}
	}
}

However, you must take care to detect if the adjustment should or should not be made. This can be as simple as checking if a deprecated FName is set / a new FName is not set, or if a deprecated TArray has any elements / a new TArray has no elements, for example. However, more complex cases may require introducing a version number as a member of the struct itself. Unfortunately, this may be necessary in your case, since there is no simple way to tell if the adjustment must be made or not (unless your new enumeration can have a specific entry for “Not Set”, for example). If you decide to introduce a version number, you can try something similar to this:

USTRUCT()
struct FStatementData
{
	GENERATED_BODY()
 
	UPROPERTY()
	int32 Version = 0;
 
	UPROPERTY()
	FString Description;
 
#if WITH_EDITORONLY_DATA
	UE_DEPRECATED(MyVersion, "IsTruthful is deprecated, please use Truthfulness instead")
	UPROPERTY(meta = (DeprecatedProperty, DeprecationMessage = "IsTruthful is deprecated, please use Truthfulness instead"))
	bool IsTruthful_DEPRECATED;
#endif
 
	UPROPERTY()
	ETruthfulness Truthfulness;
};
 
bool FStatementData::Serialize(FArchive& Ar)
{
	// Always save with the latest version number
	if (Ar.IsSaving() && Ar.IsPersistent())
		Version = 1;
 
	// Allow default serialization
	return false;
}
 
void FStatementData::PostSerialize(const FArchive& Ar)
{
#if WITH_EDITORONLY_DATA
	if (Ar.IsLoading() && Ar.IsPersistent())
	{
		// Upgrade from unversioned to version 1
		if (Version < 1)
		{
			PRAGMA_DISABLE_DEPRECATION_WARNINGS
		
			Truthfulness = IsTruthful_DEPRECATED ? ETruthfulness::True : ETruthfulness::False;
		
			PRAGMA_ENABLE_DEPRECATION_WARNINGS
		}
	}
#endif
}

Finally, as you can see above, you can wrap your deprecated members, and the backwards-compatibility code that accesses them, in a #if WITH_EDITORONLY_DATA macro block, so that the cooked game will not pay an unneccessary memory cost.

If you need something more powerful, the engine also has a built-in versioning system using FCustomVersionRegistration, FArchiveState::CustomVer() and FArchive::UsingCustomVersion(), along with functions Serialize() and PostSerialize() mentioned above. If you want to take a look, some built-in structs that use it are: FGameplayAttribute, FCommonInputActionDataBase, FStateTreeStateLink, FAnimCurveBase (and many others).

I hope this is helpful. Please let me know if you have further questions.

Best regards,

Vitor

Hi Vitor,

Thanks for your answer.

I am assuming that to prevent `bool IsTruthful_DEPRECATED` from living in editor data, I should force resave instance before getting rid of the deprecation code, right?

Thank you!

D

Hi David,

Sorry about the delay. Deprecated properties will be loaded from serialized archives, but not saved back into them. So, if you resave the assets that reference your struct while some of its properties are marked as deprecated, these deprecated properties should be gone for good.

Deprecation-handling code should remain in place while there is any chance that some asset might be loaded from an archive where the deprecated properties can be found. If you are sure that you have resaved every single asset that referenced the old version of the struct, then you can get rid of that code if desired. Please be careful, though: if you do get rid of the deprecation-handling code, resaving not-yet-converted assets will cause the loss of the deprecated data.

Best regards,

Vitor