feature: add support for relative view model data bind paths for nested artboards (#11344) 923b32059d
feature: add support for relative view model data bind paths

Co-authored-by: hernan <hernan@rive.app>
diff --git a/.rive_head b/.rive_head
index a74c1ee..c5efcd5 100644
--- a/.rive_head
+++ b/.rive_head
@@ -1 +1 @@
-699b891b79a7af1220616e0900a3d4d5d1c40448
+923b32059d2ea012fdb47ae141b304f57095a48b
diff --git a/dev/defs/data_bind/data_bind_path.json b/dev/defs/data_bind/data_bind_path.json
new file mode 100644
index 0000000..52fab4b
--- /dev/null
+++ b/dev/defs/data_bind/data_bind_path.json
@@ -0,0 +1,29 @@
+{
+  "name": "DataBindPath",
+  "key": {
+    "int": 643,
+    "string": "databindpath"
+  },
+  "properties": {
+    "path": {
+      "type": "List<Id>",
+      "typeRuntime": "Bytes",
+      "encoded": true,
+      "initialValue": "[]",
+      "key": {
+        "int": 920,
+        "string": "path"
+      },
+      "description": "Path to the selected property"
+    },
+    "isRelative": {
+      "type": "bool",
+      "initialValue": "false",
+      "key": {
+        "int": 921,
+        "string": "isrelative"
+      },
+      "description": "Whether the data bind path is relative"
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/nested_artboard.json b/dev/defs/nested_artboard.json
index 11dc6eb..4910497 100644
--- a/dev/defs/nested_artboard.json
+++ b/dev/defs/nested_artboard.json
@@ -29,6 +29,16 @@
       },
       "description": "Path to the selected property."
     },
+    "isDataBindPathRelative": {
+      "type": "bool",
+      "initialValue": "false",
+      "key": {
+        "int": 916,
+        "string": "isdatabindpathrelative"
+      },
+      "description": "Whether the data bind path is relative",
+      "runtime": false
+    },
     "isPaused": {
       "type": "bool",
       "initialValue": "false",
diff --git a/include/rive/artboard_host.hpp b/include/rive/artboard_host.hpp
index 93cb158..32657a6 100644
--- a/include/rive/artboard_host.hpp
+++ b/include/rive/artboard_host.hpp
@@ -2,6 +2,7 @@
 #define _RIVE_ARTBOARD_HOST_HPP_
 #include "rive/refcnt.hpp"
 #include "rive/file.hpp"
+#include "rive/data_bind_path_referencer.hpp"
 #include <stdio.h>
 namespace rive
 {
@@ -10,12 +11,11 @@
 class DataContext;
 class ViewModelInstance;
 
-class ArtboardHost
+class ArtboardHost : public DataBindPathReferencer
 {
 public:
     virtual size_t artboardCount() = 0;
     virtual ArtboardInstance* artboardInstance(int index = 0) = 0;
-    virtual std::vector<uint32_t> dataBindPathIds() { return {}; }
     virtual void internalDataContext(DataContext* dataContext) = 0;
     virtual void bindViewModelInstance(rcp<ViewModelInstance> viewModelInstance,
                                        DataContext* parent) = 0;
diff --git a/include/rive/assets/manifest_asset.hpp b/include/rive/assets/manifest_asset.hpp
index 59c4802..2489dc2 100644
--- a/include/rive/assets/manifest_asset.hpp
+++ b/include/rive/assets/manifest_asset.hpp
@@ -1,17 +1,21 @@
 #ifndef _RIVE_MANIFEST_ASSET_HPP_
 #define _RIVE_MANIFEST_ASSET_HPP_
 #include "rive/generated/assets/manifest_asset_base.hpp"
-#include "rive/name_resolver.hpp"
+#include "rive/data_resolver.hpp"
 #include <stdio.h>
 #include <unordered_map>
 #include <string>
 namespace rive
 {
-class ManifestAsset : public ManifestAssetBase, public NameResolver
+class ManifestAsset : public ManifestAssetBase, public DataResolver
 {
 private:
     std::unordered_map<int, std::string> m_names;
+    std::unordered_map<int, std::vector<uint32_t>> m_paths;
     static const std::string empty;
+    static const std::vector<uint32_t> emptyIntVector;
+    bool decodeNames(BinaryReader& reader);
+    bool decodePaths(BinaryReader& reader);
 
 protected:
     bool addsToBackboard() override { return false; }
@@ -20,6 +24,7 @@
     bool decode(SimpleArray<uint8_t>&, Factory*) override;
     std::string fileExtension() const override;
     const std::string& resolveName(int id) override;
+    const std::vector<uint32_t>& resolvePath(int id) override;
 };
 } // namespace rive
 
diff --git a/include/rive/data_bind/data_bind_context.hpp b/include/rive/data_bind/data_bind_context.hpp
index 45b069e..818f8e7 100644
--- a/include/rive/data_bind/data_bind_context.hpp
+++ b/include/rive/data_bind/data_bind_context.hpp
@@ -17,6 +17,10 @@
     void decodeSourcePathIds(Span<const uint8_t> value) override;
     void copySourcePathIds(const DataBindContextBase& object) override;
     void bindFromContext(DataContext* dataContext);
+
+private:
+    void resolvePath();
+    bool m_isPathResolved = false;
 };
 } // namespace rive
 
diff --git a/include/rive/data_bind/data_bind_path.hpp b/include/rive/data_bind/data_bind_path.hpp
new file mode 100644
index 0000000..589d291
--- /dev/null
+++ b/include/rive/data_bind/data_bind_path.hpp
@@ -0,0 +1,29 @@
+#ifndef _RIVE_DATA_BIND_PATH_HPP_
+#define _RIVE_DATA_BIND_PATH_HPP_
+#include "rive/generated/data_bind/data_bind_path_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class File;
+class DataBindPath : public DataBindPathBase
+{
+public:
+    void decodePath(Span<const uint8_t> value) override;
+    void copyPath(const DataBindPathBase& object) override;
+    StatusCode import(ImportStack& importStack) override;
+    std::vector<uint32_t>& path() { return m_pathBuffer; }
+    const std::vector<uint32_t>& resolvedPath();
+    void file(File* value);
+    File* file() { return m_file; };
+    void resolved(bool value) { m_resolved = value; }
+
+protected:
+    std::vector<uint32_t> m_pathBuffer;
+
+private:
+    File* m_file = nullptr;
+    bool m_resolved = false;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/data_bind/data_context.hpp b/include/rive/data_bind/data_context.hpp
index 9919eeb..12b7a1e 100644
--- a/include/rive/data_bind/data_context.hpp
+++ b/include/rive/data_bind/data_context.hpp
@@ -2,11 +2,12 @@
 #define _RIVE_DATA_CONTEXT_HPP_
 #include "rive/viewmodel/viewmodel_instance_value.hpp"
 #include "rive/viewmodel/viewmodel_instance.hpp"
-#include "rive/name_resolver.hpp"
+#include "rive/data_resolver.hpp"
 #include "rive/refcnt.hpp"
 
 namespace rive
 {
+class DataBindPath;
 class DataContext
 {
 private:
@@ -22,9 +23,13 @@
         const std::vector<uint32_t> path) const;
     ViewModelInstanceValue* getRelativeViewModelProperty(
         const std::vector<uint32_t> path,
-        NameResolver* resolver) const;
+        DataResolver* resolver) const;
     rcp<ViewModelInstance> getViewModelInstance(
         const std::vector<uint32_t> path) const;
+    rcp<ViewModelInstance> getViewModelInstance(DataBindPath*) const;
+    rcp<ViewModelInstance> getRelativeViewModelInstance(
+        const std::vector<uint32_t> path,
+        DataResolver* resolver) const;
     void viewModelInstance(rcp<ViewModelInstance> value);
     void advanced();
     rcp<ViewModelInstance> viewModelInstance() { return m_ViewModelInstance; };
diff --git a/include/rive/data_bind_path_referencer.hpp b/include/rive/data_bind_path_referencer.hpp
new file mode 100644
index 0000000..429a61f
--- /dev/null
+++ b/include/rive/data_bind_path_referencer.hpp
@@ -0,0 +1,23 @@
+#ifndef _RIVE_DATA_BIND_PATH_REFERENCER_HPP_
+#define _RIVE_DATA_BIND_PATH_REFERENCER_HPP_
+#include <stdio.h>
+#include "rive/data_bind/data_bind_path.hpp"
+namespace rive
+{
+class File;
+class DataBindPathReferencer
+{
+public:
+    ~DataBindPathReferencer();
+    DataBindPath* dataBindPath() const { return m_dataBindPath; }
+
+    void copyDataBindPath(DataBindPath* dataBindPath);
+    void importDataBindPath(ImportStack& importStack);
+    void decodeDataBindPath(Span<const uint8_t>& value);
+
+protected:
+    DataBindPath* m_dataBindPath = nullptr;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/data_resolver.hpp b/include/rive/data_resolver.hpp
new file mode 100644
index 0000000..75d4764
--- /dev/null
+++ b/include/rive/data_resolver.hpp
@@ -0,0 +1,15 @@
+#ifndef _RIVE_DATA_RESOLVER_HPP_
+#define _RIVE_DATA_RESOLVER_HPP_
+#include <stdio.h>
+#include <string>
+namespace rive
+{
+class DataResolver
+{
+public:
+    virtual const std::string& resolveName(int id) = 0;
+    virtual const std::vector<uint32_t>& resolvePath(int id) = 0;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/file.hpp b/include/rive/file.hpp
index 4ecfb79..93215f8 100644
--- a/include/rive/file.hpp
+++ b/include/rive/file.hpp
@@ -16,7 +16,7 @@
 #include "rive/animation/keyframe_interpolator.hpp"
 #include "rive/data_bind/converters/data_converter.hpp"
 #include "rive/refcnt.hpp"
-#include "rive/name_resolver.hpp"
+#include "rive/data_resolver.hpp"
 #include <vector>
 #include <set>
 #include <unordered_map>
@@ -194,7 +194,7 @@
     }
 #endif
 
-    NameResolver* nameResolver()
+    DataResolver* dataResolver()
     {
         if (m_manifest)
         {
diff --git a/include/rive/generated/core_registry.hpp b/include/rive/generated/core_registry.hpp
index aa07807..78acbdb 100644
--- a/include/rive/generated/core_registry.hpp
+++ b/include/rive/generated/core_registry.hpp
@@ -181,6 +181,7 @@
 #include "rive/data_bind/converters/formula/formula_token_value.hpp"
 #include "rive/data_bind/data_bind.hpp"
 #include "rive/data_bind/data_bind_context.hpp"
+#include "rive/data_bind/data_bind_path.hpp"
 #include "rive/draw_rules.hpp"
 #include "rive/draw_target.hpp"
 #include "rive/drawable.hpp"
@@ -666,6 +667,8 @@
                 return new ScriptInputString();
             case BindablePropertyArtboardBase::typeKey:
                 return new BindablePropertyArtboard();
+            case DataBindPathBase::typeKey:
+                return new DataBindPath();
             case BindablePropertyIntegerBase::typeKey:
                 return new BindablePropertyInteger();
             case BindablePropertyTriggerBase::typeKey:
@@ -1740,6 +1743,9 @@
             case LayoutComponentBase::clipPropertyKey:
                 object->as<LayoutComponentBase>()->clip(value);
                 break;
+            case DataBindPathBase::isRelativePropertyKey:
+                object->as<DataBindPathBase>()->isRelative(value);
+                break;
             case BindablePropertyBooleanBase::propertyValuePropertyKey:
                 object->as<BindablePropertyBooleanBase>()->propertyValue(value);
                 break;
@@ -3114,6 +3120,8 @@
                 return object->as<CustomPropertyBooleanBase>()->propertyValue();
             case LayoutComponentBase::clipPropertyKey:
                 return object->as<LayoutComponentBase>()->clip();
+            case DataBindPathBase::isRelativePropertyKey:
+                return object->as<DataBindPathBase>()->isRelative();
             case BindablePropertyBooleanBase::propertyValuePropertyKey:
                 return object->as<BindablePropertyBooleanBase>()
                     ->propertyValue();
@@ -3880,6 +3888,7 @@
             case ClippingShapeBase::isVisiblePropertyKey:
             case CustomPropertyBooleanBase::propertyValuePropertyKey:
             case LayoutComponentBase::clipPropertyKey:
+            case DataBindPathBase::isRelativePropertyKey:
             case BindablePropertyBooleanBase::propertyValuePropertyKey:
             case TextModifierRangeBase::clampPropertyKey:
             case TextFollowPathModifierBase::radialPropertyKey:
@@ -4112,6 +4121,7 @@
             case StateMachineFireTriggerBase::viewModelPathIdsPropertyKey:
             case StateMachineListenerBase::viewModelPathIdsPropertyKey:
             case MeshBase::triangleIndexBytesPropertyKey:
+            case DataBindPathBase::pathPropertyKey:
             case DataConverterOperationViewModelBase::sourcePathIdsPropertyKey:
             case DataBindContextBase::sourcePathIdsPropertyKey:
             case FileAssetBase::cdnUuidPropertyKey:
@@ -4700,6 +4710,8 @@
                 return object->is<CustomPropertyBooleanBase>();
             case LayoutComponentBase::clipPropertyKey:
                 return object->is<LayoutComponentBase>();
+            case DataBindPathBase::isRelativePropertyKey:
+                return object->is<DataBindPathBase>();
             case BindablePropertyBooleanBase::propertyValuePropertyKey:
                 return object->is<BindablePropertyBooleanBase>();
             case TextModifierRangeBase::clampPropertyKey:
diff --git a/include/rive/generated/data_bind/data_bind_path_base.hpp b/include/rive/generated/data_bind/data_bind_path_base.hpp
new file mode 100644
index 0000000..03627df
--- /dev/null
+++ b/include/rive/generated/data_bind/data_bind_path_base.hpp
@@ -0,0 +1,80 @@
+#ifndef _RIVE_DATA_BIND_PATH_BASE_HPP_
+#define _RIVE_DATA_BIND_PATH_BASE_HPP_
+#include "rive/core.hpp"
+#include "rive/core/field_types/core_bool_type.hpp"
+#include "rive/core/field_types/core_bytes_type.hpp"
+#include "rive/span.hpp"
+namespace rive
+{
+class DataBindPathBase : public Core
+{
+protected:
+    typedef Core Super;
+
+public:
+    static const uint16_t typeKey = 643;
+
+    /// Helper to quickly determine if a core object extends another without
+    /// RTTI at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case DataBindPathBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t pathPropertyKey = 920;
+    static const uint16_t isRelativePropertyKey = 921;
+
+protected:
+    bool m_IsRelative = false;
+
+public:
+    virtual void decodePath(Span<const uint8_t> value) = 0;
+    virtual void copyPath(const DataBindPathBase& object) = 0;
+
+    inline bool isRelative() const { return m_IsRelative; }
+    void isRelative(bool value)
+    {
+        if (m_IsRelative == value)
+        {
+            return;
+        }
+        m_IsRelative = value;
+        isRelativeChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const DataBindPathBase& object)
+    {
+        copyPath(object);
+        m_IsRelative = object.m_IsRelative;
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case pathPropertyKey:
+                decodePath(CoreBytesType::deserialize(reader));
+                return true;
+            case isRelativePropertyKey:
+                m_IsRelative = CoreBoolType::deserialize(reader);
+                return true;
+        }
+        return false;
+    }
+
+protected:
+    virtual void pathChanged() {}
+    virtual void isRelativeChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/importers/data_bind_path_importer.hpp b/include/rive/importers/data_bind_path_importer.hpp
new file mode 100644
index 0000000..9902e20
--- /dev/null
+++ b/include/rive/importers/data_bind_path_importer.hpp
@@ -0,0 +1,20 @@
+#ifndef _RIVE_DATA_BIND_PATH_IMPORTER_HPP_
+#define _RIVE_DATA_BIND_PATH_IMPORTER_HPP_
+
+#include "rive/importers/import_stack.hpp"
+
+namespace rive
+{
+class Core;
+class DataBindPath;
+class DataBindPathImporter : public ImportStackObject
+{
+private:
+    DataBindPath* m_dataBindPath;
+
+public:
+    DataBindPathImporter(DataBindPath* object);
+    DataBindPath* claim();
+};
+} // namespace rive
+#endif
diff --git a/include/rive/manifest_sections.hpp b/include/rive/manifest_sections.hpp
index fb73b57..3160681 100644
--- a/include/rive/manifest_sections.hpp
+++ b/include/rive/manifest_sections.hpp
@@ -6,6 +6,7 @@
 enum class ManifestSections : unsigned char
 {
     names = 0,
+    paths = 1,
 };
 }
 #endif
diff --git a/include/rive/name_resolver.hpp b/include/rive/name_resolver.hpp
deleted file mode 100644
index 24ea0cf..0000000
--- a/include/rive/name_resolver.hpp
+++ /dev/null
@@ -1,14 +0,0 @@
-#ifndef _RIVE_NAME_RESOLVER_HPP_
-#define _RIVE_NAME_RESOLVER_HPP_
-#include <stdio.h>
-#include <string>
-namespace rive
-{
-class NameResolver
-{
-public:
-    virtual const std::string& resolveName(int id) = 0;
-};
-} // namespace rive
-
-#endif
\ No newline at end of file
diff --git a/include/rive/nested_artboard.hpp b/include/rive/nested_artboard.hpp
index 828b58c..715ada7 100644
--- a/include/rive/nested_artboard.hpp
+++ b/include/rive/nested_artboard.hpp
@@ -3,6 +3,7 @@
 
 #include "rive/generated/nested_artboard_base.hpp"
 #include "rive/artboard_host.hpp"
+#include "rive/data_bind_path_referencer.hpp"
 #include "rive/data_bind/data_context.hpp"
 #include "rive/viewmodel/viewmodel_instance_value.hpp"
 #include "rive/hit_info.hpp"
@@ -38,8 +39,6 @@
     DataContext* m_dataContext = nullptr;
 
 protected:
-    std::vector<uint32_t> m_DataBindPathIdsBuffer;
-
 private:
     Artboard* findArtboard(
         ViewModelInstanceArtboard* viewModelInstanceArtboard);
@@ -92,10 +91,6 @@
     bool worldToLocal(Vec2D world, Vec2D* local);
     void decodeDataBindPathIds(Span<const uint8_t> value) override;
     void copyDataBindPathIds(const NestedArtboardBase& object) override;
-    std::vector<uint32_t> dataBindPathIds() override
-    {
-        return m_DataBindPathIdsBuffer;
-    };
     void bindViewModelInstance(rcp<ViewModelInstance> viewModelInstance,
                                DataContext* parent) override;
     void internalDataContext(DataContext* dataContext) override;
diff --git a/src/artboard.cpp b/src/artboard.cpp
index e1b6962..7af0b97 100644
--- a/src/artboard.cpp
+++ b/src/artboard.cpp
@@ -1799,8 +1799,8 @@
     m_DataContext = value;
     for (auto artboardHost : m_ArtboardHosts)
     {
-        auto value = m_DataContext->getViewModelInstance(
-            artboardHost->dataBindPathIds());
+        auto value =
+            m_DataContext->getViewModelInstance(artboardHost->dataBindPath());
         if (value != nullptr && value->is<ViewModelInstance>())
         {
             artboardHost->bindViewModelInstance(value, m_DataContext);
diff --git a/src/assets/manifest_asset.cpp b/src/assets/manifest_asset.cpp
index a99fc7c..4df0220 100644
--- a/src/assets/manifest_asset.cpp
+++ b/src/assets/manifest_asset.cpp
@@ -7,6 +7,73 @@
 using namespace rive;
 
 const std::string ManifestAsset::empty;
+const std::vector<uint32_t> ManifestAsset::emptyIntVector;
+
+bool ManifestAsset::decodeNames(BinaryReader& reader)
+{
+    // Read count of names
+    uint64_t count = reader.readVarUint64();
+    if (reader.hasError())
+    {
+        return false;
+    }
+
+    // Read all name entries
+    for (uint64_t i = 0; i < count; i++)
+    {
+        int id = static_cast<int>(reader.readVarUint64());
+        if (reader.hasError())
+        {
+            return false;
+        }
+
+        std::string value = reader.readString();
+        if (reader.hasError())
+        {
+            return false;
+        }
+
+        m_names[id] = value;
+    }
+    return true;
+}
+
+bool ManifestAsset::decodePaths(BinaryReader& reader)
+{
+    // Read count of paths
+    uint64_t count = reader.readVarUint64();
+    if (reader.hasError())
+    {
+        return false;
+    }
+
+    // Read all path entries
+    for (uint64_t i = 0; i < count; i++)
+    {
+        int id = static_cast<int>(reader.readVarUint64());
+        if (reader.hasError())
+        {
+            return false;
+        }
+        int pathLength = static_cast<int>(reader.readVarUint64());
+        if (reader.hasError())
+        {
+            return false;
+        }
+        std::vector<uint32_t> path;
+        for (uint64_t j = 0; j < pathLength; j++)
+        {
+            int pathId = static_cast<uint32_t>(reader.readVarUint64());
+            path.push_back(pathId);
+        }
+        m_paths[id] = path;
+        if (reader.hasError())
+        {
+            return false;
+        }
+    }
+    return true;
+}
 
 bool ManifestAsset::decode(SimpleArray<uint8_t>& bytes, Factory* factory)
 {
@@ -39,29 +106,17 @@
         if (static_cast<ManifestSections>(sectionValue) ==
             ManifestSections::names)
         {
-            // Read count of names
-            uint64_t count = reader.readVarUint64();
-            if (reader.hasError())
+            if (!decodeNames(reader))
             {
                 return false;
             }
-
-            // Read all name entries
-            for (uint64_t i = 0; i < count; i++)
+        }
+        else if (static_cast<ManifestSections>(sectionValue) ==
+                 ManifestSections::paths)
+        {
+            if (!decodePaths(reader))
             {
-                int id = static_cast<int>(reader.readVarUint64());
-                if (reader.hasError())
-                {
-                    return false;
-                }
-
-                std::string value = reader.readString();
-                if (reader.hasError())
-                {
-                    return false;
-                }
-
-                m_names[id] = value;
+                return false;
             }
         }
         else
@@ -96,4 +151,14 @@
         return it->second;
     }
     return empty;
+}
+
+const std::vector<uint32_t>& ManifestAsset::resolvePath(int id)
+{
+    auto it = m_paths.find(id);
+    if (it != m_paths.end())
+    {
+        return it->second;
+    }
+    return emptyIntVector;
 }
\ No newline at end of file
diff --git a/src/data_bind/data_bind_context.cpp b/src/data_bind/data_bind_context.cpp
index 63737c2..2c8540f 100644
--- a/src/data_bind/data_bind_context.cpp
+++ b/src/data_bind/data_bind_context.cpp
@@ -25,17 +25,40 @@
 void DataBindContext::copySourcePathIds(const DataBindContextBase& object)
 {
     m_SourcePathIdsBuffer = object.as<DataBindContext>()->m_SourcePathIdsBuffer;
+    m_isPathResolved = object.as<DataBindContext>()->m_isPathResolved;
+}
+
+void DataBindContext::resolvePath()
+{
+    if (!isNameBased() || m_isPathResolved)
+    {
+        return;
+    }
+    m_isPathResolved = true;
+    if (m_SourcePathIdsBuffer.size() > 0)
+    {
+        auto pathId = m_SourcePathIdsBuffer[0];
+        if (m_file)
+        {
+            auto dataResolver = m_file->dataResolver();
+            if (dataResolver)
+            {
+                m_SourcePathIdsBuffer = dataResolver->resolvePath(pathId);
+            }
+        }
+    }
 }
 
 void DataBindContext::bindFromContext(DataContext* dataContext)
 {
     if (dataContext != nullptr)
     {
+        resolvePath();
         auto vmSource =
             isNameBased() && file()
                 ? dataContext->getRelativeViewModelProperty(
                       m_SourcePathIdsBuffer,
-                      file()->nameResolver())
+                      file()->dataResolver())
                 : dataContext->getViewModelProperty(m_SourcePathIdsBuffer);
         if (vmSource != m_Source)
         {
diff --git a/src/data_bind/data_bind_path.cpp b/src/data_bind/data_bind_path.cpp
new file mode 100644
index 0000000..0241b80
--- /dev/null
+++ b/src/data_bind/data_bind_path.cpp
@@ -0,0 +1,55 @@
+#include "rive/data_bind/data_bind_path.hpp"
+#include "rive/importers/backboard_importer.hpp"
+
+using namespace rive;
+
+void DataBindPath::decodePath(Span<const uint8_t> value)
+{
+    BinaryReader reader(value);
+    while (!reader.reachedEnd())
+    {
+        auto val = reader.readVarUintAs<uint32_t>();
+        m_pathBuffer.push_back(val);
+    }
+}
+
+void DataBindPath::copyPath(const DataBindPathBase& object)
+{
+    m_pathBuffer = object.as<DataBindPath>()->m_pathBuffer;
+    m_resolved = object.as<DataBindPath>()->m_resolved;
+}
+
+StatusCode DataBindPath::import(ImportStack& importStack)
+{
+    auto backboardImporter =
+        importStack.latest<BackboardImporter>(Backboard::typeKey);
+    if (backboardImporter == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+    auto file = backboardImporter->file();
+    m_file = file;
+
+    return Super::import(importStack);
+}
+
+const std::vector<uint32_t>& DataBindPath::resolvedPath()
+{
+    if (!m_resolved)
+    {
+        if (!m_file)
+        {
+            return m_pathBuffer;
+        }
+        auto dataResolver = m_file->dataResolver();
+        if (dataResolver)
+        {
+            auto pathId = m_pathBuffer[0];
+            m_pathBuffer = dataResolver->resolvePath(pathId);
+        }
+        m_resolved = true;
+    }
+    return m_pathBuffer;
+}
+
+void DataBindPath::file(File* file) { m_file = file; }
\ No newline at end of file
diff --git a/src/data_bind/data_context.cpp b/src/data_bind/data_context.cpp
index f7dc797..7299ad4 100644
--- a/src/data_bind/data_context.cpp
+++ b/src/data_bind/data_context.cpp
@@ -1,4 +1,8 @@
 #include "rive/data_bind/data_context.hpp"
+#include "rive/data_bind/data_bind_path.hpp"
+#include "rive/file.hpp"
+#include "rive/viewmodel/viewmodel_instance.hpp"
+#include "rive/viewmodel/viewmodel.hpp"
 #include "rive/viewmodel/viewmodel_instance_viewmodel.hpp"
 
 using namespace rive;
@@ -54,7 +58,7 @@
 
 ViewModelInstanceValue* DataContext::getRelativeViewModelProperty(
     const std::vector<uint32_t> path,
-    NameResolver* resolver) const
+    DataResolver* resolver) const
 {
     std::vector<uint32_t>::const_iterator it;
     if (path.size() == 0 || !resolver)
@@ -97,11 +101,11 @@
 rcp<ViewModelInstance> DataContext::getViewModelInstance(
     const std::vector<uint32_t> path) const
 {
-    std::vector<uint32_t>::const_iterator it;
     if (path.size() == 0)
     {
         return nullptr;
     }
+    std::vector<uint32_t>::const_iterator it;
     if (m_ViewModelInstance != nullptr &&
         m_ViewModelInstance->viewModelId() == path[0])
     {
@@ -133,4 +137,74 @@
         return m_Parent->getViewModelInstance(path);
     }
     return nullptr;
+}
+
+rcp<ViewModelInstance> DataContext::getRelativeViewModelInstance(
+    const std::vector<uint32_t> path,
+    DataResolver* resolver) const
+{
+    if (path.size() == 0 || !resolver)
+    {
+        return nullptr;
+    }
+    std::vector<uint32_t>::const_iterator it;
+    if (m_ViewModelInstance != nullptr)
+    {
+        rcp<ViewModelInstance> instance = m_ViewModelInstance;
+        for (it = path.begin(); it != path.end(); it++)
+        {
+            auto viewModelInstanceValue =
+                instance->propertyValue(resolver->resolveName(*it));
+            if (viewModelInstanceValue != nullptr &&
+                viewModelInstanceValue->is<ViewModelInstanceViewModel>())
+            {
+                instance =
+                    viewModelInstanceValue->as<ViewModelInstanceViewModel>()
+                        ->referenceViewModelInstance();
+            }
+            else
+            {
+                instance = nullptr;
+            }
+            if (instance == nullptr)
+            {
+                goto skip_path_relative;
+            }
+        }
+        return instance;
+    }
+skip_path_relative:
+    if (m_Parent != nullptr)
+    {
+        return m_Parent->getRelativeViewModelInstance(path, resolver);
+    }
+    return nullptr;
+}
+
+rcp<ViewModelInstance> DataContext::getViewModelInstance(
+    DataBindPath* dataBindPath) const
+{
+    if (!dataBindPath)
+    {
+        return nullptr;
+    }
+    if (dataBindPath->isRelative())
+    {
+        auto file = dataBindPath->file();
+        if (file)
+        {
+            auto resolver = file->dataResolver();
+            if (resolver)
+            {
+                return getRelativeViewModelInstance(
+                    dataBindPath->resolvedPath(),
+                    resolver);
+            }
+        }
+        return nullptr;
+    }
+    else
+    {
+        return getViewModelInstance(dataBindPath->resolvedPath());
+    }
 }
\ No newline at end of file
diff --git a/src/data_bind_path_referencer.cpp b/src/data_bind_path_referencer.cpp
new file mode 100644
index 0000000..718ff1d
--- /dev/null
+++ b/src/data_bind_path_referencer.cpp
@@ -0,0 +1,46 @@
+#include "rive/data_bind_path_referencer.hpp"
+#include "rive/importers/data_bind_path_importer.hpp"
+#include "rive/file.hpp"
+
+using namespace rive;
+
+DataBindPathReferencer::~DataBindPathReferencer()
+{
+    if (m_dataBindPath)
+    {
+        delete m_dataBindPath;
+    }
+}
+
+void DataBindPathReferencer::copyDataBindPath(DataBindPath* dataBindPath)
+{
+    if (dataBindPath)
+    {
+        m_dataBindPath = dataBindPath->clone()->as<DataBindPath>();
+        m_dataBindPath->file(dataBindPath->file());
+    }
+}
+
+void DataBindPathReferencer::importDataBindPath(ImportStack& importStack)
+{
+    auto dataBindPathImporter =
+        importStack.latest<DataBindPathImporter>(DataBindPathBase::typeKey);
+    if (dataBindPathImporter)
+    {
+        auto dataBindPath = dataBindPathImporter->claim();
+        if (dataBindPath != nullptr)
+        {
+            // If a DataBindPath is claimed, the core object shouldn't have a
+            // path already created
+            assert(m_dataBindPath == nullptr);
+            m_dataBindPath = dataBindPath;
+        }
+    }
+}
+
+void DataBindPathReferencer::decodeDataBindPath(Span<const uint8_t>& value)
+{
+    m_dataBindPath = new DataBindPath();
+    m_dataBindPath->decodePath(value);
+    m_dataBindPath->resolved(true);
+}
\ No newline at end of file
diff --git a/src/file.cpp b/src/file.cpp
index a20be45..7e2f2da 100644
--- a/src/file.cpp
+++ b/src/file.cpp
@@ -34,6 +34,7 @@
 #include "rive/importers/viewmodel_importer.hpp"
 #include "rive/importers/viewmodel_instance_importer.hpp"
 #include "rive/importers/viewmodel_instance_list_importer.hpp"
+#include "rive/importers/data_bind_path_importer.hpp"
 #include "rive/animation/blend_state_transition.hpp"
 #include "rive/animation/any_state.hpp"
 #include "rive/animation/entry_state.hpp"
@@ -45,6 +46,7 @@
 #include "rive/animation/transition_property_viewmodel_comparator.hpp"
 #include "rive/constraints/scrolling/scroll_physics.hpp"
 #include "rive/data_bind/data_bind.hpp"
+#include "rive/data_bind/data_bind_path.hpp"
 #include "rive/data_bind/bindable_property.hpp"
 #include "rive/data_bind/bindable_property_artboard.hpp"
 #include "rive/data_bind/bindable_property_asset.hpp"
@@ -538,6 +540,7 @@
             case ScriptedDrawable::typeKey:
             case ScriptedLayout::typeKey:
             case ScriptedPathEffect::typeKey:
+            {
                 auto scriptedObject = ScriptedObject::from(object);
                 if (scriptedObject != nullptr)
                 {
@@ -546,6 +549,12 @@
                     stackType = ScriptedDrawable::typeKey;
                 }
                 break;
+            }
+            case DataBindPathBase::typeKey:
+                stackObject = rivestd::make_unique<DataBindPathImporter>(
+                    object->as<DataBindPath>());
+                stackType = DataBindPathBase::typeKey;
+                break;
         }
         if (importStack.makeLatest(stackType, std::move(stackObject)) !=
             StatusCode::Ok)
diff --git a/src/generated/data_bind/data_bind_path_base.cpp b/src/generated/data_bind/data_bind_path_base.cpp
new file mode 100644
index 0000000..4dcb383
--- /dev/null
+++ b/src/generated/data_bind/data_bind_path_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/data_bind/data_bind_path_base.hpp"
+#include "rive/data_bind/data_bind_path.hpp"
+
+using namespace rive;
+
+Core* DataBindPathBase::clone() const
+{
+    auto cloned = new DataBindPath();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/importers/data_bind_path_importer.cpp b/src/importers/data_bind_path_importer.cpp
new file mode 100644
index 0000000..9808ea8
--- /dev/null
+++ b/src/importers/data_bind_path_importer.cpp
@@ -0,0 +1,15 @@
+#include "rive/data_bind/data_bind_path.hpp"
+#include "rive/importers/data_bind_path_importer.hpp"
+
+using namespace rive;
+
+DataBindPathImporter::DataBindPathImporter(DataBindPath* obj) :
+    m_dataBindPath(obj)
+{}
+
+DataBindPath* DataBindPathImporter::claim()
+{
+    auto path = m_dataBindPath;
+    m_dataBindPath = nullptr;
+    return path;
+}
\ No newline at end of file
diff --git a/src/nested_artboard.cpp b/src/nested_artboard.cpp
index 14d907d..0759935 100644
--- a/src/nested_artboard.cpp
+++ b/src/nested_artboard.cpp
@@ -6,6 +6,7 @@
 #include "rive/importers/backboard_importer.hpp"
 #include "rive/nested_animation.hpp"
 #include "rive/animation/nested_state_machine.hpp"
+#include "rive/data_bind/data_bind_path.hpp"
 #include "rive/clip_result.hpp"
 #include <limits>
 #include <cassert>
@@ -209,6 +210,7 @@
 
 StatusCode NestedArtboard::import(ImportStack& importStack)
 {
+    importDataBindPath(importStack);
     auto backboardImporter =
         importStack.latest<BackboardImporter>(Backboard::typeKey);
     if (backboardImporter == nullptr)
@@ -378,18 +380,12 @@
 
 void NestedArtboard::decodeDataBindPathIds(Span<const uint8_t> value)
 {
-    BinaryReader reader(value);
-    while (!reader.reachedEnd())
-    {
-        auto val = reader.readVarUintAs<uint32_t>();
-        m_DataBindPathIdsBuffer.push_back(val);
-    }
+    decodeDataBindPath(value);
 }
 
 void NestedArtboard::copyDataBindPathIds(const NestedArtboardBase& object)
 {
-    m_DataBindPathIdsBuffer =
-        object.as<NestedArtboard>()->m_DataBindPathIdsBuffer;
+    copyDataBindPath(object.as<NestedArtboard>()->dataBindPath());
 }
 
 void NestedArtboard::internalDataContext(DataContext* value)
diff --git a/tests/unit_tests/assets/relative_data_bind_path.riv b/tests/unit_tests/assets/relative_data_bind_path.riv
new file mode 100644
index 0000000..a0b93d8
--- /dev/null
+++ b/tests/unit_tests/assets/relative_data_bind_path.riv
Binary files differ
diff --git a/tests/unit_tests/assets/relative_data_binding.riv b/tests/unit_tests/assets/relative_data_binding.riv
index 1979a96..038da78 100644
--- a/tests/unit_tests/assets/relative_data_binding.riv
+++ b/tests/unit_tests/assets/relative_data_binding.riv
Binary files differ
diff --git a/tests/unit_tests/runtime/data_binding_test.cpp b/tests/unit_tests/runtime/data_binding_test.cpp
index 9add394..857acb3 100644
--- a/tests/unit_tests/runtime/data_binding_test.cpp
+++ b/tests/unit_tests/runtime/data_binding_test.cpp
@@ -2001,6 +2001,51 @@
 
     CHECK(silver.matches("relative_data_binding"));
 }
+TEST_CASE("Relative data binding view model path", "[silver]")
+{
+    SerializingFactory silver;
+    auto file = ReadRiveFile("assets/relative_data_bind_path.riv", &silver);
+
+    auto artboard = file->artboardDefault();
+    REQUIRE(artboard != nullptr);
+
+    silver.frameSize(artboard->width(), artboard->height());
+
+    auto stateMachine = artboard->stateMachineAt(0);
+    auto renderer = silver.makeRenderer();
+    // First use the default view model instance that is attached to the
+    // artboard
+    {
+        auto vmi =
+            file->createViewModelInstance((int)artboard.get()->viewModelId(),
+                                          0);
+
+        stateMachine->bindViewModelInstance(vmi);
+        stateMachine->advanceAndApply(0.1f);
+        artboard->draw(renderer.get());
+        silver.addFrame();
+    }
+    // Next bind it to a different view model type that matches the expected
+    // view model shape
+    {
+        auto vm = file->viewModel("ViewModel1");
+        auto vmi = file->createDefaultViewModelInstance(vm);
+        stateMachine->bindViewModelInstance(vmi);
+        stateMachine->advanceAndApply(0.1f);
+        artboard->draw(renderer.get());
+        silver.addFrame();
+    }
+    // Next bind it to a different view model with the wrong shape
+    {
+        auto vm = file->viewModel("ViewModel2");
+        auto vmi = file->createDefaultViewModelInstance(vm);
+        stateMachine->bindViewModelInstance(vmi);
+        stateMachine->advanceAndApply(0.1f);
+        artboard->draw(renderer.get());
+    }
+
+    CHECK(silver.matches("relative_data_bind_path"));
+}
 
 TEST_CASE("Listen to view model value changes in state machines", "[silver]")
 {
diff --git a/tests/unit_tests/silvers/relative_data_bind_path.sriv b/tests/unit_tests/silvers/relative_data_bind_path.sriv
new file mode 100644
index 0000000..6b30c73
--- /dev/null
+++ b/tests/unit_tests/silvers/relative_data_bind_path.sriv
Binary files differ