How to Create a Generic Struct-to-JSON Converter Node

We all know that debugging simple or complex Structs in Unreal can be a total nightmare. I wanted to share this simple function that I always carry with me to any new project. It can convert any Struct into a clean and readable JSON Object in string format, so you can easily debug your data.

The Formatting Nightmare

Let’s say you have a Struct and you want to print it to the screen to see if the data is correct. Usually you do it this way:

  • Break the struct.
  • Create a FormatText node and build the debug message manually.
  • Connect every pin which the data it holds might require conversion.
  • If you have an array or a map then you need to create a loop node and print every element in that array individually.

The Problem: If you add a new variable to that Struct later, you have to go back and update every debug node in your Blueprints manually. And you need to redo the same logic for every struct you want to debug and print to the log.

Keep it simple

All this chaos can be replaced with a simple and tiny function, it takes advantage of CustomThunk. In short, when you mark a UFUNCTION with CustomThunk, you’re telling the engine’s Unreal Header Tool (UHT) to skip the automatic generation of the function body and that you will be providing a custom implementation to handle the Blueprint Virtual Machine (BPVM) stack operations yourself.
Here is how to use this function:

So easy right !! and it is also formatted in a beautiful JSNO object.

How to Implement it

First, you need to enable the JSON modules in your project. Open your [ProjectName].Build.cs file and add "Json" and "JsonUtilities".

// MyProject.Build.cs
PublicDependencyModuleNames.AddRange(new string[] {
    "Core",
    "CoreUObject",
    "Engine",
    "InputCore",
    "Json",           // <--- Add this
    "JsonUtilities"   // <--- Add this
});

Then create a Blueprint Function Library class if you don’t have one for your project, and create this function ConvertStructToJsonString in the header file (.h) as shown below.

// Copyright 2025 Cool Glitch, Inc. All Rights Reserved.

#pragma once

#include "TutorialFunctionLibrary.generated.h"

#define UE_API TUTORIAL_API

UCLASS(MinimalAPI)
class UTutorialFunctionLibrary : public UBlueprintFunctionLibrary
{
    GENERATED_BODY()

public:
    /**
     * Converts any Struct type into a formatted JSON Object String.
     * Useful for debugging, logging, or sending data to a server.
     *
     * @param InStruct The wildcard struct to convert.
     * @param OutJsonString The resulting string in JSON format.
     */
    UFUNCTION(
        BlueprintPure,
        CustomThunk,
        meta = (CustomStructureParam = "InStruct", AutoCreateRefTerm = "InStruct"),
        Category = "Tutorial|Debug")
    static UE_API void ConvertStructToJsonString(
        UPARAM(DisplayName="Struct") const int32& InStruct,
        UPARAM(DisplayName="JsonString") FString& OutJsonString);

    /** The internal execution body for the CustomThunk. */
    DECLARE_FUNCTION(execConvertStructToJsonString);

};

#undef UE_API

Because this is a CustomThunk, we don’t write a standard C++ function body. Instead, we write a DEFINE_FUNCTION that reads the memory stack directly and accesses the Blueprint Virtual Machine.
Define the function as follows. I added some comments inside the code so you can follow along with what is happening inside the function.

// Copyright 2025 Cool Glitch, Inc. All Rights Reserved.

#include "Utils/TutorialFunctionLibrary.h"
#include "JsonObjectConverter.h"
#include "Blueprint/BlueprintExceptionInfo.h"

#define LOCTEXT_NAMESPACE "TutorialFunctionLibrary"

#include UE_INLINE_GENERATED_CPP_BY_NAME(TutorialFunctionLibrary)

DEFINE_FUNCTION(UTutorialFunctionLibrary::execConvertStructToJsonString)
{
    // Reset pointers to ensure we don't carry over garbage from previous steps.
    Stack.MostRecentPropertyAddress = nullptr;
    Stack.MostRecentPropertyContainer = nullptr;
    // Read Wildcard Input (InStruct)
    Stack.StepCompiledIn<FStructProperty>(nullptr);
    const FStructProperty* ValueProp = CastField<FStructProperty>(Stack.MostRecentProperty);
    const void* ValuePtr = Stack.MostRecentPropertyAddress;

    Stack.MostRecentPropertyAddress = nullptr;
    Stack.MostRecentPropertyContainer = nullptr;
    // Step into the second parameter (OutJsonString).
    Stack.StepCompiledIn<FStrProperty>(nullptr);

    //  We expect a String Property Reference only.
    void* OutStringAddr = Stack.MostRecentPropertyAddress;
    FString* OutStringPtr = static_cast<FString*>(OutStringAddr);

    // Finish Blueprint vm Stack
    P_FINISH;

    // If any pointers are null, we cannot proceed safely.
    if(!ValueProp || !ValuePtr || !OutStringPtr)
    {
        // Define the runtime exception
        const FBlueprintExceptionInfo ExceptionInfo(
            EBlueprintExceptionType::AbortExecution,
            LOCTEXT(
                "Tutorial_JsonError",
                "Failed to resolve parameters for ConvertStructToJson. Ensure a valid struct is connected.")
        );

        // Throw the exception to the Blueprint VM
        FBlueprintCoreDelegates::ThrowScriptException(P_THIS, Stack, ExceptionInfo);

        P_NATIVE_BEGIN;
            // Safe failure state: Return an empty object string
            if(OutStringPtr)
            {
                *OutStringPtr = TEXT("{}");
            }
        P_NATIVE_END;
    }
    else
    {
        P_NATIVE_BEGIN;
            // Perform the conversion safely within the native block
            FJsonObjectConverter::UStructToJsonObjectString(ValueProp->Struct.Get(), ValuePtr, *OutStringPtr);
        P_NATIVE_END;
    }
}

#undef LOCTEXT_NAMESPACE

Conclusion

With this simple function, you can now just drop this node whenever you need to inspect a Struct’s data. No more manual formatting for every struct in your project, and no more updating debug functions when your struct changes!