diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7815b9679fa..99d0666b921 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
 ### Enhancements
 * <New feature description> (PR [#????](https://github.com/realm/realm-core/pull/????))
 * Storage of integers changed so that they take up less space in the file. This can cause commits and some queries to take a bit longer (PR [#7668](https://github.com/realm/realm-core/pull/7668))
+* We now allow synchronizing, getting and setting properties that are not defined in the object schema (PR [#7886](https://github.com/realm/realm-core/pull/7886))
 
 ### Fixed
 * <How do the end-user experience this issue? what was the impact?> ([#????](https://github.com/realm/realm-core/issues/????), since v?.?.?)
diff --git a/bindgen/spec.yml b/bindgen/spec.yml
index 91fac6394fa..80304576ca2 100644
--- a/bindgen/spec.yml
+++ b/bindgen/spec.yml
@@ -440,6 +440,9 @@ records:
       schema_mode:
         type: SchemaMode
         default: SchemaMode::Automatic
+      flexible_schema:
+        type: bool
+        default: false
       disable_format_upgrade:
         type: bool
         default: false
diff --git a/src/realm.h b/src/realm.h
index 68e39b80111..d043fb8b3e8 100644
--- a/src/realm.h
+++ b/src/realm.h
@@ -925,6 +925,11 @@ RLM_API bool realm_config_get_cached(realm_config_t*) RLM_API_NOEXCEPT;
  */
 RLM_API void realm_config_set_automatic_backlink_handling(realm_config_t*, bool) RLM_API_NOEXCEPT;
 
+/**
+ * Allow realm objects in the realm to have additional properties that are not defined in the schema.
+ */
+RLM_API void realm_config_set_flexible_schema(realm_config_t*, bool) RLM_API_NOEXCEPT;
+
 /**
  * Create a custom scheduler object from callback functions.
  *
@@ -1640,6 +1645,13 @@ RLM_API realm_object_t* realm_object_from_thread_safe_reference(const realm_t*,
  */
 RLM_API bool realm_get_value(const realm_object_t*, realm_property_key_t, realm_value_t* out_value);
 
+/**
+ * Get the value for a property.
+ *
+ * @return True if no exception occurred.
+ */
+RLM_API bool realm_get_value_by_name(const realm_object_t*, const char* property_name, realm_value_t* out_value);
+
 /**
  * Get the values for several properties.
  *
@@ -1675,6 +1687,41 @@ RLM_API bool realm_get_values(const realm_object_t*, size_t num_values, const re
  */
 RLM_API bool realm_set_value(realm_object_t*, realm_property_key_t, realm_value_t new_value, bool is_default);
 
+/**
+ * Set the value for a property. Property need not be defined in schema if flexible
+ * schema is enabled in configuration
+ *
+ * @param property_name The name of the property.
+ * @param new_value The new value for the property.
+ * @return True if no exception occurred.
+ */
+RLM_API bool realm_set_value_by_name(realm_object_t*, const char* property_name, realm_value_t new_value);
+
+/**
+ * Examines if the object has a property with the given name.
+ * @param out_has_property will be true if the property exists.
+ * @return True if no exception occurred.
+ */
+RLM_API bool realm_has_property(realm_object_t*, const char* property_name, bool* out_has_property);
+
+/**
+ * Get a list of properties set on the object that are not defined in the schema.
+ *
+ * @param out_prop_names A pointer to an array of const char* of size @a max. If the pointer is NULL,
+ *                       no names will be copied, but @a out_n will be set to the required size.
+ * @param max size of @a out_prop_names
+ * @param out_n number of names actually returned.
+ */
+RLM_API void realm_get_additional_properties(realm_object_t*, const char** out_prop_names, size_t max, size_t* out_n);
+
+/**
+ * Erases a property from an object. You can't erase a property that is defined in the current schema.
+ *
+ * @param property_name The name of the property.
+ * @return True if the property was removed.
+ */
+RLM_API bool realm_erase_additional_property(realm_object_t*, const char* property_name);
+
 /**
  * Assign a JSON formatted string to a Mixed property. Underlying structures will be created as needed
  *
@@ -1696,6 +1743,8 @@ RLM_API realm_object_t* realm_set_embedded(realm_object_t*, realm_property_key_t
  */
 RLM_API realm_list_t* realm_set_list(realm_object_t*, realm_property_key_t);
 RLM_API realm_dictionary_t* realm_set_dictionary(realm_object_t*, realm_property_key_t);
+RLM_API realm_list_t* realm_set_list_by_name(realm_object_t*, const char* property_name);
+RLM_API realm_dictionary_t* realm_set_dictionary_by_name(realm_object_t*, const char* property_name);
 
 /** Return the object linked by the given property
  *
@@ -1748,6 +1797,15 @@ RLM_API bool realm_set_values(realm_object_t*, size_t num_values, const realm_pr
  */
 RLM_API realm_list_t* realm_get_list(realm_object_t*, realm_property_key_t);
 
+/**
+ * Get a list instance for the property of an object by name.
+ *
+ * Note: It is up to the caller to call `realm_release()` on the returned list.
+ *
+ * @return A non-null pointer if no exception occurred.
+ */
+RLM_API realm_list_t* realm_get_list_by_name(realm_object_t*, const char*);
+
 /**
  * Create a `realm_list_t` from a pointer to a `realm::List`, copy-constructing
  * the internal representation.
@@ -2253,6 +2311,15 @@ RLM_API realm_set_t* realm_set_from_thread_safe_reference(const realm_t*, realm_
  */
 RLM_API realm_dictionary_t* realm_get_dictionary(realm_object_t*, realm_property_key_t);
 
+/**
+ * Get a dictionary instance for the property of an object by name.
+ *
+ * Note: It is up to the caller to call `realm_release()` on the returned dictionary.
+ *
+ * @return A non-null pointer if no exception occurred.
+ */
+RLM_API realm_dictionary_t* realm_get_dictionary_by_name(realm_object_t*, const char*);
+
 /**
  * Create a `realm_dictionary_t` from a pointer to a `realm::object_store::Dictionary`,
  * copy-constructing the internal representation.
diff --git a/src/realm/db.cpp b/src/realm/db.cpp
index f6f258616d2..6198cac10ae 100644
--- a/src/realm/db.cpp
+++ b/src/realm/db.cpp
@@ -2806,7 +2806,8 @@ void DB::async_request_write_mutex(TransactionRef& tr, util::UniqueFunction<void
 }
 
 inline DB::DB(Private, const DBOptions& options)
-    : m_upgrade_callback(std::move(options.upgrade_callback))
+    : m_allow_flexible_schema(options.allow_flexible_schema)
+    , m_upgrade_callback(std::move(options.upgrade_callback))
     , m_log_id(util::gen_log_id(this))
 {
     if (options.enable_async_writes) {
diff --git a/src/realm/db.hpp b/src/realm/db.hpp
index 39e5f3452d4..72db6a2a5cb 100644
--- a/src/realm/db.hpp
+++ b/src/realm/db.hpp
@@ -498,6 +498,7 @@ class DB : public std::enable_shared_from_this<DB> {
     SharedInfo* m_info = nullptr;
     bool m_wait_for_change_enabled = true; // Initially wait_for_change is enabled
     bool m_write_transaction_open GUARDED_BY(m_mutex) = false;
+    bool m_allow_flexible_schema;
     std::string m_db_path;
     int m_file_format_version = 0;
     util::InterprocessMutex m_writemutex;
diff --git a/src/realm/db_options.hpp b/src/realm/db_options.hpp
index a11eded2352..68e2c8098f3 100644
--- a/src/realm/db_options.hpp
+++ b/src/realm/db_options.hpp
@@ -106,6 +106,9 @@ struct DBOptions {
     /// will clear and reinitialize the file.
     bool clear_on_invalid_file = false;
 
+    /// Allow setting properties not supported by a specific column on an object
+    bool allow_flexible_schema = false;
+
     /// sys_tmp_dir will be used if the temp_dir is empty when creating DBOptions.
     /// It must be writable and allowed to create pipe/fifo file on it.
     /// set_sys_tmp_dir is not a thread-safe call and it is only supposed to be called once
diff --git a/src/realm/exec/realm_trawler.cpp b/src/realm/exec/realm_trawler.cpp
index d6882d76192..ea32365d532 100644
--- a/src/realm/exec/realm_trawler.cpp
+++ b/src/realm/exec/realm_trawler.cpp
@@ -979,6 +979,12 @@ class HistoryLogger {
         return true;
     }
 
+    bool modify_object(const std::string& prop_name, realm::ObjKey key)
+    {
+        std::cout << "Modify object: " << prop_name << " on " << key << std::endl;
+        return true;
+    }
+
     bool collection_set(size_t ndx)
     {
         std::cout << "Collection set at " << ndx << std::endl;
diff --git a/src/realm/group.cpp b/src/realm/group.cpp
index b6703b3af53..f0c941725c7 100644
--- a/src/realm/group.cpp
+++ b/src/realm/group.cpp
@@ -69,12 +69,13 @@ Group::Group()
 }
 
 
-Group::Group(const std::string& file_path, const char* encryption_key)
+Group::Group(const std::string& file_path, const char* encryption_key, bool allow_additional_properties)
     : m_local_alloc(new SlabAlloc) // Throws
     , m_alloc(*m_local_alloc)
     , m_top(m_alloc)
     , m_tables(m_alloc)
     , m_table_names(m_alloc)
+    , m_allow_additional_properties(allow_additional_properties)
 {
     init_array_parents();
 
@@ -760,6 +761,10 @@ Table* Group::do_add_table(StringData name, Table::Type table_type, bool do_repl
     Table* table = create_table_accessor(j);
     table->do_set_table_type(table_type);
 
+    if (m_allow_additional_properties && name.begins_with(g_class_name_prefix)) {
+        table->do_add_additional_prop_column();
+    }
+
     return table;
 }
 
diff --git a/src/realm/group.hpp b/src/realm/group.hpp
index 352c5bd25fb..a28509ae135 100644
--- a/src/realm/group.hpp
+++ b/src/realm/group.hpp
@@ -117,7 +117,8 @@ class Group : public ArrayParent {
     /// types that are derived from FileAccessError, the
     /// derived exception type is thrown. Note that InvalidDatabase is
     /// among these derived exception types.
-    explicit Group(const std::string& file, const char* encryption_key = nullptr);
+    explicit Group(const std::string& file, const char* encryption_key = nullptr,
+                   bool allow_additional_properties = false);
 
     /// Attach this Group instance to the specified memory buffer.
     ///
@@ -599,6 +600,7 @@ class Group : public ArrayParent {
     mutable int m_num_tables = 0;
     bool m_attached = false;
     bool m_is_writable = true;
+    bool m_allow_additional_properties = false;
     static std::optional<int> fake_target_file_format;
 
     util::UniqueFunction<void(const CascadeNotification&)> m_notify_handler;
diff --git a/src/realm/impl/array_writer.hpp b/src/realm/impl/array_writer.hpp
index 4096805e0fa..1b2694a8e60 100644
--- a/src/realm/impl/array_writer.hpp
+++ b/src/realm/impl/array_writer.hpp
@@ -30,9 +30,7 @@ class ArrayWriterBase {
     bool only_modified = true;
     bool compress = true;
     const Table* table;
-    virtual ~ArrayWriterBase()
-    {
-    }
+    virtual ~ArrayWriterBase() {}
 
     /// Write the specified array data and its checksum into free
     /// space.
diff --git a/src/realm/impl/transact_log.hpp b/src/realm/impl/transact_log.hpp
index 335856d6719..c457dabdad1 100644
--- a/src/realm/impl/transact_log.hpp
+++ b/src/realm/impl/transact_log.hpp
@@ -168,6 +168,10 @@ class NullInstructionObserver {
     {
         return true;
     }
+    bool modify_object(const std::string&, ObjKey)
+    {
+        return true;
+    }
 
     // Must have descriptor selected:
     bool insert_column(ColKey)
@@ -240,6 +244,7 @@ class TransactLogEncoder {
         return true;
     }
     bool modify_object(ColKey col_key, ObjKey key);
+    bool modify_object(StringData prop_name, ObjKey key);
 
     // Must have descriptor selected:
     bool insert_column(ColKey col_key);
@@ -324,7 +329,7 @@ class TransactLogEncoder {
     void append_simple_instr(L... numbers);
 
     template <typename... L>
-    void append_string_instr(Instruction, StringData);
+    void append_string_instr(Instruction, StringData, L... numbers);
 
     template <class T>
     static char* encode_int(char*, T value);
@@ -574,15 +579,17 @@ void TransactLogEncoder::append_simple_instr(L... numbers)
 }
 
 template <typename... L>
-void TransactLogEncoder::append_string_instr(Instruction instr, StringData string)
+void TransactLogEncoder::append_string_instr(Instruction instr, StringData string, L... numbers)
 {
-    size_t max_required_bytes = 1 + max_enc_bytes_per_int + string.size();
+    size_t max_required_bytes = 1 + max_enc_bytes_per_int + string.size() + max_size_list(numbers...);
+    ;
     char* ptr = reserve(max_required_bytes); // Throws
     *ptr++ = char(instr);
-    ptr = encode(ptr, int(type_String));
+    ptr = encode(ptr, ColKey{}.value);
     ptr = encode(ptr, size_t(string.size()));
     ptr = std::copy(string.data(), string.data() + string.size(), ptr);
     advance(ptr);
+    encode_list(ptr, numbers...);
 }
 
 inline bool TransactLogEncoder::insert_group_level_table(TableKey table_key)
@@ -627,6 +634,12 @@ inline bool TransactLogEncoder::modify_object(ColKey col_key, ObjKey key)
     return true;
 }
 
+inline bool TransactLogEncoder::modify_object(StringData prop_name, ObjKey key)
+{
+    append_string_instr(instr_Set, prop_name, key); // Throws
+    return true;
+}
+
 
 /************************************ Collections ***********************************/
 
@@ -697,9 +710,20 @@ void TransactLogParser::parse_one(InstructionHandler& handler)
     switch (instr) {
         case instr_Set: {
             ColKey col_key = ColKey(read_int<int64_t>()); // Throws
+            StringData prop;
+            if (!col_key) {
+                // Setting an additional property
+                prop = read_string(m_string_buffer);
+            }
             ObjKey key(read_int<int64_t>());              // Throws
-            if (!handler.modify_object(col_key, key))     // Throws
-                parser_error();
+            if (col_key) {
+                if (!handler.modify_object(col_key, key)) // Throws
+                    parser_error();
+            }
+            else {
+                if (!handler.modify_object(std::string(prop), key)) // Throws
+                    parser_error();
+            }
             return;
         }
         case instr_SetDefault:
@@ -1061,6 +1085,10 @@ class NoOpTransactionLogParser {
     {
         return false;
     }
+    bool modify_object(std::string&&, ObjKey)
+    {
+        return false;
+    }
     bool typed_link_change(ColKey, TableKey)
     {
         return true;
diff --git a/src/realm/obj.cpp b/src/realm/obj.cpp
index 8d968971fc7..5b95813fbda 100644
--- a/src/realm/obj.cpp
+++ b/src/realm/obj.cpp
@@ -631,6 +631,30 @@ BinaryData Obj::_get<BinaryData>(ColKey::Idx col_ndx) const
     return ArrayBinary::get(alloc.translate(ref), m_row_ndx, alloc);
 }
 
+bool Obj::has_property(StringData prop_name) const
+{
+    if (m_table->get_column_key(prop_name))
+        return true;
+    if (auto ck = m_table->m_additional_prop_col) {
+        Dictionary dict(*this, ck);
+        return dict.contains(prop_name);
+    }
+    return false;
+}
+
+std::vector<StringData> Obj::get_additional_properties() const
+{
+    std::vector<StringData> ret;
+
+    if (auto ck = m_table->m_additional_prop_col) {
+        Dictionary dict(*this, ck);
+        dict.for_all_keys<StringData>([&ret](StringData key) {
+            ret.push_back(key);
+        });
+    }
+    return ret;
+}
+
 Mixed Obj::get_any(ColKey col_key) const
 {
     m_table->check_column(col_key);
@@ -676,6 +700,19 @@ Mixed Obj::get_any(ColKey col_key) const
     return {};
 }
 
+Mixed Obj::get_additional_prop(StringData prop_name) const
+{
+    if (auto ck = m_table->m_additional_prop_col) {
+        Dictionary dict(*this, ck);
+        if (auto val = dict.try_get(prop_name)) {
+            return *val;
+        }
+    }
+    throw InvalidArgument(ErrorCodes::InvalidProperty,
+                          util::format("Property '%1.%2' does not exist", m_table->get_class_name(), prop_name));
+    return {};
+}
+
 Mixed Obj::get_primary_key() const
 {
     auto col = m_table->get_primary_key_column();
@@ -1107,7 +1144,8 @@ StablePath Obj::get_stable_path() const noexcept
 void Obj::add_index(Path& path, const CollectionParent::Index& index) const
 {
     if (path.empty()) {
-        path.emplace_back(get_table()->get_column_key(index));
+        auto ck = m_table->get_column_key(index);
+        path.emplace_back(ck);
     }
     else {
         StringData col_name = get_table()->get_column_name(index);
@@ -1229,6 +1267,32 @@ Obj& Obj::set<Mixed>(ColKey col_key, Mixed value, bool is_default)
     return *this;
 }
 
+Obj& Obj::erase_additional_prop(StringData prop_name)
+{
+    bool erased = false;
+    if (auto ck = m_table->m_additional_prop_col) {
+        Dictionary dict(*this, ck);
+        erased = dict.try_erase(prop_name);
+    }
+    if (!erased) {
+        throw InvalidArgument(ErrorCodes::InvalidProperty, util::format("Could not erase property: %1", prop_name));
+    }
+    return *this;
+}
+
+Obj& Obj::set_additional_prop(StringData prop_name, const Mixed& value)
+{
+    if (auto ck = m_table->m_additional_prop_col) {
+        Dictionary dict(*this, ck);
+        dict.insert(prop_name, value);
+    }
+    else {
+        throw InvalidArgument(ErrorCodes::InvalidProperty,
+                              util::format("Property '%1.%2' does not exist", m_table->get_class_name(), prop_name));
+    }
+    return *this;
+}
+
 Obj& Obj::set_any(ColKey col_key, Mixed value, bool is_default)
 {
     if (value.is_null()) {
@@ -1983,7 +2047,6 @@ Dictionary Obj::get_dictionary(ColKey col_key) const
 
 Obj& Obj::set_collection(ColKey col_key, CollectionType type)
 {
-    REALM_ASSERT(col_key.get_type() == col_type_Mixed);
     if ((col_key.is_dictionary() && type == CollectionType::Dictionary) ||
         (col_key.is_list() && type == CollectionType::List)) {
         return *this;
@@ -1991,11 +2054,34 @@ Obj& Obj::set_collection(ColKey col_key, CollectionType type)
     if (type == CollectionType::Set) {
         throw IllegalOperation("Set nested in Mixed is not supported");
     }
+    if (col_key.get_type() != col_type_Mixed) {
+        throw IllegalOperation("Collection can only be nested in Mixed");
+    }
     set(col_key, Mixed(0, type));
 
     return *this;
 }
 
+Obj& Obj::set_collection(StringData prop_name, CollectionType type)
+{
+    if (auto ck = get_column_key(prop_name)) {
+        return set_collection(ck, type);
+    }
+    return set_additional_collection(prop_name, type);
+}
+
+Obj& Obj::set_additional_collection(StringData prop_name, CollectionType type)
+{
+    if (auto ck = m_table->m_additional_prop_col) {
+        Dictionary dict(*this, ck);
+        dict.insert_collection(prop_name, type);
+    }
+    else {
+        throw InvalidArgument(ErrorCodes::InvalidProperty, util::format("Property not found: %1", prop_name));
+    }
+    return *this;
+}
+
 DictionaryPtr Obj::get_dictionary_ptr(ColKey col_key) const
 {
     return std::make_shared<Dictionary>(get_dictionary(col_key));
@@ -2011,14 +2097,33 @@ Dictionary Obj::get_dictionary(StringData col_name) const
     return get_dictionary(get_column_key(col_name));
 }
 
-CollectionPtr Obj::get_collection_ptr(const Path& path) const
+CollectionBasePtr Obj::get_collection_ptr(const Path& path) const
 {
     REALM_ASSERT(path.size() > 0);
     // First element in path must be column name
     auto col_key = path[0].is_col_key() ? path[0].get_col_key() : m_table->get_column_key(path[0].get_key());
-    REALM_ASSERT(col_key);
+
+    CollectionBasePtr collection;
     size_t level = 1;
-    CollectionBasePtr collection = get_collection_ptr(col_key);
+    if (col_key) {
+        collection = get_collection_ptr(col_key);
+    }
+    else {
+        if (auto ck = m_table->m_additional_prop_col) {
+            auto prop_name = path[0].get_key();
+            Dictionary dict(*this, ck);
+            auto ref = dict.get(prop_name);
+            if (ref.is_type(type_List)) {
+                collection = dict.get_list(prop_name);
+            }
+            else if (ref.is_type(type_Dictionary)) {
+                collection = dict.get_dictionary(prop_name);
+            }
+            else {
+                throw InvalidArgument("Wrong path");
+            }
+        }
+    }
 
     while (level < path.size()) {
         auto& path_elem = path[level];
@@ -2044,7 +2149,7 @@ CollectionPtr Obj::get_collection_ptr(const Path& path) const
     return collection;
 }
 
-CollectionPtr Obj::get_collection_by_stable_path(const StablePath& path) const
+CollectionBasePtr Obj::get_collection_by_stable_path(const StablePath& path) const
 {
     // First element in path is phony column key
     ColKey col_key = m_table->get_column_key(path[0]);
@@ -2108,7 +2213,7 @@ CollectionBasePtr Obj::get_collection_ptr(ColKey col_key) const
 
 CollectionBasePtr Obj::get_collection_ptr(StringData col_name) const
 {
-    return get_collection_ptr(get_column_key(col_name));
+    return get_collection_ptr(Path{{col_name}});
 }
 
 LinkCollectionPtr Obj::get_linkcollection_ptr(ColKey col_key) const
diff --git a/src/realm/obj.hpp b/src/realm/obj.hpp
index 67c82a0cada..ac4cdcf7c4b 100644
--- a/src/realm/obj.hpp
+++ b/src/realm/obj.hpp
@@ -117,17 +117,29 @@ class Obj {
     template <typename U>
     U get(ColKey col_key) const;
 
+    bool has_property(StringData prop_name) const;
+
+    std::vector<StringData> get_additional_properties() const;
+
     Mixed get_any(ColKey col_key) const;
     Mixed get_any(StringData col_name) const
     {
-        return get_any(get_column_key(col_name));
+        if (auto ck = get_column_key(col_name)) {
+            return get_any(ck);
+        }
+        return get_additional_prop(col_name);
     }
+    Mixed get_additional_prop(StringData col_name) const;
+
     Mixed get_primary_key() const;
 
     template <typename U>
     U get(StringData col_name) const
     {
-        return get<U>(get_column_key(col_name));
+        if (auto ck = get_column_key(col_name)) {
+            return get<U>(ck);
+        }
+        return get_additional_prop(col_name).get<U>();
     }
     bool is_unresolved(ColKey col_key) const;
 
@@ -187,17 +199,26 @@ class Obj {
     // default state. If the object does not exist, create a
     // new object and link it. (To Be Implemented)
     Obj clear_linked_object(ColKey col_key);
+
+    Obj& erase_additional_prop(StringData prop_name);
     Obj& set_any(ColKey col_key, Mixed value, bool is_default = false);
     Obj& set_any(StringData col_name, Mixed value, bool is_default = false)
     {
-        return set_any(get_column_key(col_name), value, is_default);
+        if (auto ck = get_column_key(col_name)) {
+            return set_any(ck, value, is_default);
+        }
+        return set_additional_prop(col_name, value);
     }
 
     template <typename U>
     Obj& set(StringData col_name, U value, bool is_default = false)
     {
-        return set(get_column_key(col_name), value, is_default);
+        if (auto ck = get_column_key(col_name)) {
+            return set(ck, value, is_default);
+        }
+        return set_additional_prop(col_name, Mixed(value));
     }
+    Obj& set_additional_prop(StringData prop_name, const Mixed& value);
 
     Obj& set_null(ColKey col_key, bool is_default = false);
     Obj& set_null(StringData col_name, bool is_default = false)
@@ -206,6 +227,7 @@ class Obj {
     }
     Obj& set_json(ColKey col_key, StringData json);
 
+
     Obj& add_int(ColKey col_key, int64_t value);
     Obj& add_int(StringData col_name, int64_t value)
     {
@@ -248,6 +270,11 @@ class Obj {
     {
         return std::dynamic_pointer_cast<Lst<U>>(get_collection_ptr(path));
     }
+    template <typename U>
+    std::shared_ptr<Lst<U>> get_list_ptr(StringData prop_name) const
+    {
+        return get_list_ptr<U>(Path{prop_name});
+    }
 
     template <typename U>
     Lst<U> get_list(StringData col_name) const
@@ -285,17 +312,24 @@ class Obj {
     LnkSet get_linkset(StringData col_name) const;
     LnkSetPtr get_linkset_ptr(ColKey col_key) const;
     SetBasePtr get_setbase_ptr(ColKey col_key) const;
+
     Dictionary get_dictionary(ColKey col_key) const;
     Dictionary get_dictionary(StringData col_name) const;
 
     Obj& set_collection(ColKey col_key, CollectionType type);
+    Obj& set_collection(StringData, CollectionType type);
+    Obj& set_additional_collection(StringData, CollectionType type);
     DictionaryPtr get_dictionary_ptr(ColKey col_key) const;
     DictionaryPtr get_dictionary_ptr(const Path& path) const;
+    DictionaryPtr get_dictionary_ptr(StringData prop_name) const
+    {
+        return get_dictionary_ptr(Path{prop_name});
+    }
 
     CollectionBasePtr get_collection_ptr(ColKey col_key) const;
     CollectionBasePtr get_collection_ptr(StringData col_name) const;
-    CollectionPtr get_collection_ptr(const Path& path) const;
-    CollectionPtr get_collection_by_stable_path(const StablePath& path) const;
+    CollectionBasePtr get_collection_ptr(const Path& path) const;
+    CollectionBasePtr get_collection_by_stable_path(const StablePath& path) const;
     LinkCollectionPtr get_linkcollection_ptr(ColKey col_key) const;
 
     void assign_pk_and_backlinks(Obj& other);
diff --git a/src/realm/object-store/c_api/config.cpp b/src/realm/object-store/c_api/config.cpp
index 7b1b00498c2..2d76bd9368f 100644
--- a/src/realm/object-store/c_api/config.cpp
+++ b/src/realm/object-store/c_api/config.cpp
@@ -243,3 +243,8 @@ RLM_API void realm_config_set_automatic_backlink_handling(realm_config_t* realm_
 {
     realm_config->automatically_handle_backlinks_in_migrations = enable_automatic_handling;
 }
+
+RLM_API void realm_config_set_flexible_schema(realm_config_t* realm_config, bool flexible_schema) noexcept
+{
+    realm_config->flexible_schema = flexible_schema;
+}
diff --git a/src/realm/object-store/c_api/object.cpp b/src/realm/object-store/c_api/object.cpp
index 44bc55ab548..4971a818c66 100644
--- a/src/realm/object-store/c_api/object.cpp
+++ b/src/realm/object-store/c_api/object.cpp
@@ -253,31 +253,31 @@ RLM_API realm_object_t* realm_object_from_thread_safe_reference(const realm_t* r
     });
 }
 
-RLM_API bool realm_get_value(const realm_object_t* obj, realm_property_key_t col, realm_value_t* out_value)
+RLM_API bool realm_get_value(const realm_object_t* object, realm_property_key_t col, realm_value_t* out_value)
 {
-    return realm_get_values(obj, 1, &col, out_value);
+    return realm_get_values(object, 1, &col, out_value);
 }
 
-RLM_API bool realm_get_values(const realm_object_t* obj, size_t num_values, const realm_property_key_t* properties,
+RLM_API bool realm_get_values(const realm_object_t* object, size_t num_values, const realm_property_key_t* properties,
                               realm_value_t* out_values)
 {
     return wrap_err([&]() {
-        obj->verify_attached();
+        object->verify_attached();
 
-        auto o = obj->get_obj();
+        auto obj = object->get_obj();
 
         for (size_t i = 0; i < num_values; ++i) {
             auto col_key = ColKey(properties[i]);
 
             if (col_key.is_collection()) {
-                auto table = o.get_table();
-                auto& schema = schema_for_table(obj->get_realm(), table->get_key());
+                auto table = obj.get_table();
+                auto& schema = schema_for_table(object->get_realm(), table->get_key());
                 throw PropertyTypeMismatch{schema.name, table->get_column_name(col_key)};
             }
 
-            auto val = o.get_any(col_key);
+            auto val = obj.get_any(col_key);
             if (out_values) {
-                auto converted = objkey_to_typed_link(val, col_key, *o.get_table());
+                auto converted = objkey_to_typed_link(val, col_key, *obj.get_table());
                 out_values[i] = to_capi(converted);
             }
         }
@@ -286,18 +286,34 @@ RLM_API bool realm_get_values(const realm_object_t* obj, size_t num_values, cons
     });
 }
 
-RLM_API bool realm_set_value(realm_object_t* obj, realm_property_key_t col, realm_value_t new_value, bool is_default)
+RLM_API bool realm_get_value_by_name(const realm_object_t* object, const char* property_name,
+                                     realm_value_t* out_value)
+{
+    return wrap_err([&]() {
+        object->verify_attached();
+
+        auto obj = object->get_obj();
+        auto val = obj.get_any(property_name);
+        if (out_value) {
+            *out_value = to_capi(val);
+        }
+        return true;
+    });
+}
+
+RLM_API bool realm_set_value(realm_object_t* object, realm_property_key_t col, realm_value_t new_value,
+                             bool is_default)
 {
-    return realm_set_values(obj, 1, &col, &new_value, is_default);
+    return realm_set_values(object, 1, &col, &new_value, is_default);
 }
 
-RLM_API bool realm_set_values(realm_object_t* obj, size_t num_values, const realm_property_key_t* properties,
+RLM_API bool realm_set_values(realm_object_t* object, size_t num_values, const realm_property_key_t* properties,
                               const realm_value_t* values, bool is_default)
 {
     return wrap_err([&]() {
-        obj->verify_attached();
-        auto o = obj->get_obj();
-        auto table = o.get_table();
+        object->verify_attached();
+        auto obj = object->get_obj();
+        auto table = obj.get_table();
 
         // Perform validation up front to avoid partial updates. This is
         // unlikely to incur performance overhead because the object itself is
@@ -308,12 +324,12 @@ RLM_API bool realm_set_values(realm_object_t* obj, size_t num_values, const real
             table->check_column(col_key);
 
             if (col_key.is_collection()) {
-                auto& schema = schema_for_table(obj->get_realm(), table->get_key());
+                auto& schema = schema_for_table(object->get_realm(), table->get_key());
                 throw PropertyTypeMismatch{schema.name, table->get_column_name(col_key)};
             }
 
             auto val = from_capi(values[i]);
-            check_value_assignable(obj->get_realm(), *table, col_key, val);
+            check_value_assignable(object->get_realm(), *table, col_key, val);
         }
 
         // Actually write the properties.
@@ -321,36 +337,94 @@ RLM_API bool realm_set_values(realm_object_t* obj, size_t num_values, const real
         for (size_t i = 0; i < num_values; ++i) {
             auto col_key = ColKey(properties[i]);
             auto val = from_capi(values[i]);
-            o.set_any(col_key, val, is_default);
+            obj.set_any(col_key, val, is_default);
         }
 
         return true;
     });
 }
 
-RLM_API bool realm_set_json(realm_object_t* obj, realm_property_key_t col, const char* json_string)
+RLM_API bool realm_set_value_by_name(realm_object_t* object, const char* property_name, realm_value_t new_value)
 {
     return wrap_err([&]() {
-        obj->verify_attached();
-        auto o = obj->get_obj();
+        object->verify_attached();
+        auto obj = object->get_obj();
+        obj.set_any(property_name, from_capi(new_value));
+        return true;
+    });
+}
+
+RLM_API bool realm_has_property(realm_object_t* object, const char* property_name, bool* out_has_property)
+{
+    return wrap_err([&]() {
+        object->verify_attached();
+        if (out_has_property) {
+            auto obj = object->get_obj();
+            *out_has_property = obj.has_property(property_name);
+        }
+        return true;
+    });
+}
+
+RLM_API void realm_get_additional_properties(realm_object_t* object, const char** out_prop_names, size_t max,
+                                             size_t* out_n)
+{
+    size_t copied = 0;
+    wrap_err([&]() {
+        object->verify_attached();
+        auto obj = object->get_obj();
+        auto vec = obj.get_additional_properties();
+        copied = vec.size();
+        if (out_prop_names) {
+            if (max < copied) {
+                copied = max;
+            }
+            auto it = vec.begin();
+            auto to_copy = copied;
+            while (to_copy--) {
+                *out_prop_names++ = (*it++).data();
+            }
+        }
+        return true;
+    });
+    if (out_n) {
+        *out_n = copied;
+    }
+}
+
+RLM_API bool realm_erase_additional_property(realm_object_t* object, const char* property_name)
+{
+    return wrap_err([&]() {
+        object->verify_attached();
+        auto obj = object->get_obj();
+        obj.erase_additional_prop(property_name);
+        return true;
+    });
+}
+
+RLM_API bool realm_set_json(realm_object_t* object, realm_property_key_t col, const char* json_string)
+{
+    return wrap_err([&]() {
+        object->verify_attached();
+        auto obj = object->get_obj();
         ColKey col_key(col);
         if (col_key.get_type() != col_type_Mixed) {
-            auto table = o.get_table();
-            auto& schema = schema_for_table(obj->get_realm(), table->get_key());
+            auto table = obj.get_table();
+            auto& schema = schema_for_table(object->get_realm(), table->get_key());
             throw PropertyTypeMismatch{schema.name, table->get_column_name(col_key)};
         }
-        o.set_json(ColKey(col), json_string);
+        obj.set_json(ColKey(col), json_string);
         return true;
     });
 }
 
 
-RLM_API realm_object_t* realm_set_embedded(realm_object_t* obj, realm_property_key_t col)
+RLM_API realm_object_t* realm_set_embedded(realm_object_t* object, realm_property_key_t col)
 {
     return wrap_err([&]() {
-        obj->verify_attached();
-        auto& o = obj->get_obj();
-        return new realm_object_t({obj->get_realm(), o.create_and_set_linked_object(ColKey(col))});
+        object->verify_attached();
+        auto& obj = object->get_obj();
+        return new realm_object_t({object->get_realm(), obj.create_and_set_linked_object(ColKey(col))});
     });
 }
 
@@ -380,12 +454,35 @@ RLM_API realm_dictionary_t* realm_set_dictionary(realm_object_t* object, realm_p
     });
 }
 
-RLM_API realm_object_t* realm_get_linked_object(realm_object_t* obj, realm_property_key_t col)
+RLM_API realm_list_t* realm_set_list_by_name(realm_object_t* object, const char* property_name)
 {
     return wrap_err([&]() {
-        obj->verify_attached();
-        const auto& o = obj->get_obj().get_linked_object(ColKey(col));
-        return o ? new realm_object_t({obj->get_realm(), o}) : nullptr;
+        object->verify_attached();
+
+        auto& obj = object->get_obj();
+        obj.set_collection(property_name, CollectionType::List);
+        return new realm_list_t{List{object->get_realm(), obj.get_list_ptr<Mixed>(property_name)}};
+    });
+}
+
+RLM_API realm_dictionary_t* realm_set_dictionary_by_name(realm_object_t* object, const char* property_name)
+{
+    return wrap_err([&]() {
+        object->verify_attached();
+
+        auto& obj = object->get_obj();
+        obj.set_collection(property_name, CollectionType::Dictionary);
+        return new realm_dictionary_t{
+            object_store::Dictionary{object->get_realm(), obj.get_dictionary_ptr(property_name)}};
+    });
+}
+
+RLM_API realm_object_t* realm_get_linked_object(realm_object_t* object, realm_property_key_t col)
+{
+    return wrap_err([&]() {
+        object->verify_attached();
+        const auto& obj = object->get_obj().get_linked_object(ColKey(col));
+        return obj ? new realm_object_t({object->get_realm(), obj}) : nullptr;
     });
 }
 
@@ -408,6 +505,20 @@ RLM_API realm_list_t* realm_get_list(realm_object_t* object, realm_property_key_
     });
 }
 
+RLM_API realm_list_t* realm_get_list_by_name(realm_object_t* object, const char* prop_name)
+{
+    return wrap_err([&]() -> realm_list_t* {
+        object->verify_attached();
+
+        const auto& obj = object->get_obj();
+        auto collection = obj.get_collection_ptr(StringData(prop_name));
+        if (collection->get_collection_type() == CollectionType::List) {
+            return new realm_list_t{List{object->get_realm(), std::move(collection)}};
+        }
+        return nullptr;
+    });
+}
+
 RLM_API realm_set_t* realm_get_set(realm_object_t* object, realm_property_key_t key)
 {
     return wrap_err([&]() {
@@ -445,6 +556,20 @@ RLM_API realm_dictionary_t* realm_get_dictionary(realm_object_t* object, realm_p
     });
 }
 
+RLM_API realm_dictionary_t* realm_get_dictionary_by_name(realm_object_t* object, const char* prop_name)
+{
+    return wrap_err([&]() -> realm_dictionary_t* {
+        object->verify_attached();
+
+        const auto& obj = object->get_obj();
+        auto collection = obj.get_collection_ptr(StringData(prop_name));
+        if (collection->get_collection_type() == CollectionType::Dictionary) {
+            return new realm_dictionary_t{object_store::Dictionary{object->get_realm(), std::move(collection)}};
+        }
+        return nullptr;
+    });
+}
+
 RLM_API char* realm_object_to_string(realm_object_t* object)
 {
     return wrap_err([&]() {
diff --git a/src/realm/object-store/collection_notifications.hpp b/src/realm/object-store/collection_notifications.hpp
index d484e73cd02..e45f7ae075f 100644
--- a/src/realm/object-store/collection_notifications.hpp
+++ b/src/realm/object-store/collection_notifications.hpp
@@ -105,6 +105,7 @@ struct CollectionChangeSet {
 
     // Per-column version of `modifications`
     std::unordered_map<int64_t, IndexSet> columns;
+    std::unordered_map<std::string, IndexSet> additional_properties;
 
     std::set<StableIndex> paths;
 
diff --git a/src/realm/object-store/impl/collection_change_builder.cpp b/src/realm/object-store/impl/collection_change_builder.cpp
index 66f9923bcae..255d3d69634 100644
--- a/src/realm/object-store/impl/collection_change_builder.cpp
+++ b/src/realm/object-store/impl/collection_change_builder.cpp
@@ -239,6 +239,7 @@ void CollectionChangeBuilder::clear(size_t old_size)
     insertions.clear();
     moves.clear();
     columns.clear();
+    additional_properties.clear();
     deletions.set(old_size);
     collection_was_cleared = true;
 }
@@ -704,5 +705,5 @@ CollectionChangeSet CollectionChangeBuilder::finalize() &&
 
     return {std::move(deletions),     std::move(insertions), std::move(modifications_in_old),
             std::move(modifications), std::move(moves),      collection_root_was_deleted,
-            collection_was_cleared,   std::move(columns)};
+            collection_was_cleared,   std::move(columns),    std::move(additional_properties)};
 }
diff --git a/src/realm/object-store/impl/object_notifier.cpp b/src/realm/object-store/impl/object_notifier.cpp
index 676680017d8..c8b17b6365e 100644
--- a/src/realm/object-store/impl/object_notifier.cpp
+++ b/src/realm/object-store/impl/object_notifier.cpp
@@ -106,7 +106,12 @@ void ObjectNotifier::run()
 
     // Finally we add all changes to `m_change` which is later used to notify about the changed columns.
     m_change.modifications.add(0);
-    for (auto col : *column_modifications) {
-        m_change.columns[col.value].add(0);
+    for (auto prop : *column_modifications) {
+        if (prop.is_col()) {
+            m_change.columns[prop.get_col().value].add(0);
+        }
+        else {
+            m_change.additional_properties[prop.get_name()].add(0);
+        }
     }
 }
diff --git a/src/realm/object-store/impl/realm_coordinator.cpp b/src/realm/object-store/impl/realm_coordinator.cpp
index 3d9f2a95034..06a47e6041c 100644
--- a/src/realm/object-store/impl/realm_coordinator.cpp
+++ b/src/realm/object-store/impl/realm_coordinator.cpp
@@ -478,6 +478,7 @@ bool RealmCoordinator::open_db()
         options.durability = m_config.in_memory ? DBOptions::Durability::MemOnly : DBOptions::Durability::Full;
         options.is_immutable = m_config.immutable();
         options.logger = util::Logger::get_default_logger();
+        options.allow_flexible_schema = m_config.flexible_schema;
 
         if (!m_config.fifo_files_fallback_path.empty()) {
             options.temp_dir = util::normalize_dir(m_config.fifo_files_fallback_path);
diff --git a/src/realm/object-store/impl/transact_log_handler.cpp b/src/realm/object-store/impl/transact_log_handler.cpp
index 93377cfaa06..08fbebfbf91 100644
--- a/src/realm/object-store/impl/transact_log_handler.cpp
+++ b/src/realm/object-store/impl/transact_log_handler.cpp
@@ -113,7 +113,7 @@ void KVOAdapter::before(Transaction& sg)
         auto column_modifications = table.get_columns_modified(key);
         if (column_modifications) {
             for (auto col : *column_modifications) {
-                observer.changes[col.value].kind = BindingContext::ColumnInfo::Kind::Set;
+                observer.changes[col.get_col().value].kind = BindingContext::ColumnInfo::Kind::Set;
             }
         }
     }
@@ -322,6 +322,10 @@ struct TransactLogValidator : public TransactLogValidationMixin {
     {
         return true;
     }
+    bool modify_object(const std::string&, ObjKey)
+    {
+        return true;
+    }
     bool select_collection(ColKey, ObjKey, const StablePath&)
     {
         return true;
@@ -468,6 +472,13 @@ class TransactLogObserver : public TransactLogValidationMixin {
         return true;
     }
 
+    bool modify_object(std::string&& prop_name, ObjKey key)
+    {
+        if (m_active_table)
+            m_active_table->modifications_add(key, std::move(prop_name));
+        return true;
+    }
+
     bool insert_column(ColKey)
     {
         m_info.schema_changed = true;
diff --git a/src/realm/object-store/object.hpp b/src/realm/object-store/object.hpp
index 53fa43a2653..8d5a7527f7d 100644
--- a/src/realm/object-store/object.hpp
+++ b/src/realm/object-store/object.hpp
@@ -206,7 +206,12 @@ class Object {
     void set_property_value_impl(ContextType& ctx, const Property& property, ValueType value, CreatePolicy policy,
                                  bool is_default);
     template <typename ValueType, typename ContextType>
+    void set_additional_property_value_impl(ContextType& ctx, StringData prop_name, ValueType value,
+                                            CreatePolicy policy);
+    template <typename ValueType, typename ContextType>
     ValueType get_property_value_impl(ContextType& ctx, const Property& property) const;
+    template <typename ValueType, typename ContextType>
+    ValueType get_additional_property_value_impl(ContextType& ctx, StringData prop_name) const;
 
     template <typename ValueType, typename ContextType>
     static ObjKey get_for_primary_key_in_migration(ContextType& ctx, Table const& table, const Property& primary_prop,
diff --git a/src/realm/object-store/object_accessor.hpp b/src/realm/object-store/object_accessor.hpp
index 824ee437204..868325c55ce 100644
--- a/src/realm/object-store/object_accessor.hpp
+++ b/src/realm/object-store/object_accessor.hpp
@@ -42,9 +42,13 @@ namespace realm {
 template <typename ValueType, typename ContextType>
 void Object::set_property_value(ContextType& ctx, StringData prop_name, ValueType value, CreatePolicy policy)
 {
-    auto& property = property_for_name(prop_name);
-    validate_property_for_setter(property);
-    set_property_value_impl(ctx, property, value, policy, false);
+    if (auto prop = m_object_schema->property_for_name(prop_name)) {
+        validate_property_for_setter(*prop);
+        set_property_value_impl(ctx, *prop, value, policy, false);
+    }
+    else {
+        set_additional_property_value_impl(ctx, prop_name, value, policy);
+    }
 }
 
 template <typename ValueType, typename ContextType>
@@ -62,7 +66,12 @@ ValueType Object::get_property_value(ContextType& ctx, const Property& property)
 template <typename ValueType, typename ContextType>
 ValueType Object::get_property_value(ContextType& ctx, StringData prop_name) const
 {
-    return get_property_value_impl<ValueType>(ctx, property_for_name(prop_name));
+    if (auto prop = m_object_schema->property_for_name(prop_name)) {
+        return get_property_value_impl<ValueType>(ctx, *prop);
+    }
+    else {
+        return get_additional_property_value_impl<ValueType>(ctx, prop_name);
+    }
 }
 
 namespace {
@@ -205,6 +214,28 @@ void Object::set_property_value_impl(ContextType& ctx, const Property& property,
     ctx.did_change();
 }
 
+template <typename ValueType, typename ContextType>
+void Object::set_additional_property_value_impl(ContextType& ctx, StringData prop_name, ValueType value,
+                                                CreatePolicy policy)
+{
+    Mixed new_val = ctx.template unbox<Mixed>(value, policy);
+    if (new_val.is_type(type_Dictionary)) {
+        m_obj.set_additional_collection(prop_name, CollectionType::Dictionary);
+        object_store::Dictionary dict(m_realm, m_obj.get_collection_ptr(prop_name));
+        dict.assign(ctx, value, policy);
+        ctx.did_change();
+        return;
+    }
+    if (new_val.is_type(type_List)) {
+        m_obj.set_additional_collection(prop_name, CollectionType::List);
+        List list(m_realm, m_obj.get_collection_ptr(prop_name));
+        list.assign(ctx, value, policy);
+        ctx.did_change();
+        return;
+    }
+    m_obj.set_additional_prop(prop_name, new_val);
+}
+
 template <typename ValueType, typename ContextType>
 ValueType Object::get_property_value_impl(ContextType& ctx, const Property& property) const
 {
@@ -269,6 +300,20 @@ ValueType Object::get_property_value_impl(ContextType& ctx, const Property& prop
     }
 }
 
+template <typename ValueType, typename ContextType>
+ValueType Object::get_additional_property_value_impl(ContextType& ctx, StringData prop_name) const
+{
+    verify_attached();
+    auto value = m_obj.get_additional_prop(prop_name);
+    if (value.is_type(type_Dictionary)) {
+        return ctx.box(object_store::Dictionary(m_realm, m_obj.get_collection_ptr(prop_name)));
+    }
+    if (value.is_type(type_List)) {
+        return ctx.box(List(m_realm, m_obj.get_collection_ptr(prop_name)));
+    }
+    return ctx.box(value);
+}
+
 template <typename ValueType, typename ContextType>
 Object Object::create(ContextType& ctx, std::shared_ptr<Realm> const& realm, StringData object_type, ValueType value,
                       CreatePolicy policy, ObjKey current_obj, Obj* out_row)
diff --git a/src/realm/object-store/object_changeset.cpp b/src/realm/object-store/object_changeset.cpp
index 922a6a8ad28..4d72d837915 100644
--- a/src/realm/object-store/object_changeset.cpp
+++ b/src/realm/object-store/object_changeset.cpp
@@ -20,6 +20,8 @@
 
 using namespace realm;
 
+std::string ObjectChangeSet::KeyOrString::empty_string;
+
 void ObjectChangeSet::insertions_add(ObjKey obj)
 {
     m_insertions.insert(obj);
@@ -33,6 +35,14 @@ void ObjectChangeSet::modifications_add(ObjKey obj, ColKey col)
     }
 }
 
+void ObjectChangeSet::modifications_add(ObjKey obj, std::string&& prop_name)
+{
+    // don't report modifications on new objects
+    if (m_insertions.find(obj) == m_insertions.end()) {
+        m_modifications[obj].insert(std::move(prop_name));
+    }
+}
+
 void ObjectChangeSet::deletions_add(ObjKey obj)
 {
     m_modifications.erase(obj);
@@ -82,7 +92,7 @@ bool ObjectChangeSet::modifications_contains(ObjKey obj, const std::vector<ColKe
     }
 
     // If a filter was set we need to check if the changed column is part of this filter.
-    const std::unordered_set<ColKey>& changed_columns_for_object = m_modifications.at(obj);
+    const std::unordered_set<KeyOrString>& changed_columns_for_object = m_modifications.at(obj);
     for (const auto& column_key_in_filter : filtered_column_keys) {
         if (changed_columns_for_object.count(column_key_in_filter)) {
             return true;
diff --git a/src/realm/object-store/object_changeset.hpp b/src/realm/object-store/object_changeset.hpp
index 0b931f743de..a2e28c44041 100644
--- a/src/realm/object-store/object_changeset.hpp
+++ b/src/realm/object-store/object_changeset.hpp
@@ -27,6 +27,7 @@
 #include <unordered_map>
 #include <unordered_set>
 #include <vector>
+#include <variant>
 
 namespace realm {
 
@@ -36,8 +37,49 @@ namespace realm {
  */
 class ObjectChangeSet {
 public:
+    class KeyOrString {
+    public:
+        KeyOrString(ColKey ck)
+            : m_col_or_string(ck)
+        {
+        }
+        KeyOrString(std::string&& prop)
+            : m_col_or_string(std::move(prop))
+        {
+        }
+        size_t hash() const noexcept
+        {
+            return std::hash<std::variant<ColKey, std::string>>{}(m_col_or_string);
+        }
+        bool operator==(const KeyOrString& other) const
+        {
+            return m_col_or_string == other.m_col_or_string;
+        }
+        bool is_col() const
+        {
+            return std::holds_alternative<ColKey>(m_col_or_string);
+        }
+        ColKey get_col() const
+        {
+            if (const ColKey* pval = std::get_if<ColKey>(&m_col_or_string)) {
+                return *pval;
+            }
+            return ColKey();
+        }
+        const std::string& get_name() const
+        {
+            if (const std::string* pval = std::get_if<std::string>(&m_col_or_string)) {
+                return *pval;
+            }
+            return empty_string;
+        }
+
+    private:
+        static std::string empty_string;
+        std::variant<ColKey, std::string> m_col_or_string;
+    };
     using ObjectSet = std::unordered_set<ObjKey>;
-    using ColumnSet = std::unordered_set<ColKey>;
+    using ColumnSet = std::unordered_set<KeyOrString>;
     using ObjectMapToColumnSet = std::unordered_map<ObjKey, ColumnSet>;
 
     ObjectChangeSet() = default;
@@ -48,6 +90,7 @@ class ObjectChangeSet {
 
     void insertions_add(ObjKey obj);
     void modifications_add(ObjKey obj, ColKey col);
+    void modifications_add(ObjKey obj, std::string&& prop_name);
     void deletions_add(ObjKey obj);
 
     bool insertions_remove(ObjKey obj);
@@ -128,4 +171,14 @@ class ObjectChangeSet {
 
 } // end namespace realm
 
+namespace std {
+template <>
+struct hash<::realm::ObjectChangeSet::KeyOrString> {
+    inline size_t operator()(const ::realm::ObjectChangeSet::KeyOrString& prop) const noexcept
+    {
+        return prop.hash();
+    }
+};
+} // namespace std
+
 #endif // REALM_OBJECT_CHANGESET_HPP
diff --git a/src/realm/object-store/shared_realm.hpp b/src/realm/object-store/shared_realm.hpp
index 5e98c96149f..23b79b1ea9a 100644
--- a/src/realm/object-store/shared_realm.hpp
+++ b/src/realm/object-store/shared_realm.hpp
@@ -104,6 +104,7 @@ struct RealmConfig {
     std::string fifo_files_fallback_path;
 
     bool in_memory = false;
+    bool flexible_schema = false;
     SchemaMode schema_mode = SchemaMode::Automatic;
     SchemaSubsetMode schema_subset_mode = SchemaSubsetMode::Strict;
 
diff --git a/src/realm/replication.cpp b/src/realm/replication.cpp
index 350eb61fd05..8678d129075 100644
--- a/src/realm/replication.cpp
+++ b/src/realm/replication.cpp
@@ -434,6 +434,18 @@ void Replication::link_list_nullify(const Lst<ObjKey>& list, size_t link_ndx)
 
 void Replication::dictionary_insert(const CollectionBase& dict, size_t ndx, Mixed key, Mixed value)
 {
+    const Table* source_table = dict.get_table().unchecked_ptr();
+    auto col = dict.get_col_key();
+
+    if (source_table->is_additional_props_col(col)) {
+        // Here we have to fake it and pretend we are setting a property on the object
+        ObjKey obj_key = dict.get_owner_key();
+        select_table(source_table); // Throws
+        if (select_obj(obj_key)) {
+            m_encoder.modify_object(key.get_string(), obj_key); // Throws
+        }
+        return;
+    }
     if (select_collection(dict)) { // Throws
         m_encoder.collection_insert(ndx);
     }
@@ -442,6 +454,18 @@ void Replication::dictionary_insert(const CollectionBase& dict, size_t ndx, Mixe
 
 void Replication::dictionary_set(const CollectionBase& dict, size_t ndx, Mixed key, Mixed value)
 {
+    const Table* source_table = dict.get_table().unchecked_ptr();
+    auto col = dict.get_col_key();
+
+    if (source_table->is_additional_props_col(col)) {
+        // Here we have to fake it and pretend we are setting a property on the object
+        ObjKey obj_key = dict.get_owner_key();
+        select_table(source_table); // Throws
+        if (select_obj(obj_key)) {
+            m_encoder.modify_object(key.get_string(), obj_key); // Throws
+        }
+        return;
+    }
     if (select_collection(dict)) { // Throws
         m_encoder.collection_set(ndx);
     }
diff --git a/src/realm/sync/instruction_applier.cpp b/src/realm/sync/instruction_applier.cpp
index 3106858f890..50bf5112ed5 100644
--- a/src/realm/sync/instruction_applier.cpp
+++ b/src/realm/sync/instruction_applier.cpp
@@ -1495,8 +1495,13 @@ InstructionApplier::PathResolver::Status InstructionApplier::PathResolver::resol
 InstructionApplier::PathResolver::Status InstructionApplier::PathResolver::resolve_field(Obj& obj, InternString field)
 {
     auto field_name = get_string(field);
-    ColKey col = obj.get_table()->get_column_key(field_name);
+    auto table = obj.get_table().unchecked_ptr();
+    ColKey col = table->get_column_key(field_name);
     if (!col) {
+        if (auto ck = table->get_additional_prop_col()) {
+            auto dict = obj.get_dictionary(ck);
+            return resolve_dictionary_element(dict, field);
+        }
         on_error(util::format("%1: No such field: '%2' in class '%3'", m_instr_name, field_name,
                               obj.get_table()->get_name()));
         return Status::DidNotResolve;
diff --git a/src/realm/sync/instruction_replication.cpp b/src/realm/sync/instruction_replication.cpp
index c845bd9c0bf..190a4c92dd3 100644
--- a/src/realm/sync/instruction_replication.cpp
+++ b/src/realm/sync/instruction_replication.cpp
@@ -611,51 +611,62 @@ void SyncReplication::set_clear(const CollectionBase& set)
     }
 }
 
-void SyncReplication::dictionary_update(const CollectionBase& dict, const Mixed& key, const Mixed& value)
+void SyncReplication::dictionary_update(const CollectionBase& dict, const Mixed& key, const Mixed* value)
 {
     // If link is unresolved, it should not be communicated.
-    if (value.is_unresolved_link()) {
+    if (value && value->is_unresolved_link()) {
         return;
     }
 
-    if (select_collection(dict)) {
-        Instruction::Update instr;
-        REALM_ASSERT(key.get_type() == type_String);
-        populate_path_instr(instr, dict);
+    Instruction::Update instr;
+    REALM_ASSERT(key.get_type() == type_String);
+
+    const Table* source_table = dict.get_table().unchecked_ptr();
+    auto col = dict.get_col_key();
+    ObjKey obj_key = dict.get_owner_key();
+    if (source_table->is_additional_props_col(col)) {
+        // Here we have to fake it and pretend we are setting/erasing a property on the object
+        if (!select_table(*source_table)) {
+            return;
+        }
+
+        populate_path_instr(instr, *source_table, obj_key, {key.get_string()});
+    }
+    else {
+        if (!select_collection(dict)) {
+            return;
+        }
+
+        populate_path_instr(instr, *source_table, obj_key, dict.get_short_path());
         StringData key_value = key.get_string();
         instr.path.push_back(m_encoder.intern_string(key_value));
-        instr.value = as_payload(dict, value);
-        instr.is_default = false;
-        emit(instr);
     }
+    if (value) {
+        instr.value = as_payload(*source_table, col, *value);
+    }
+    else {
+        instr.value = Instruction::Payload::Erased{};
+    }
+    instr.is_default = false;
+    emit(instr);
 }
 
 void SyncReplication::dictionary_insert(const CollectionBase& dict, size_t ndx, Mixed key, Mixed value)
 {
     Replication::dictionary_insert(dict, ndx, key, value);
-    dictionary_update(dict, key, value);
+    dictionary_update(dict, key, &value);
 }
 
 void SyncReplication::dictionary_set(const CollectionBase& dict, size_t ndx, Mixed key, Mixed value)
 {
     Replication::dictionary_set(dict, ndx, key, value);
-    dictionary_update(dict, key, value);
+    dictionary_update(dict, key, &value);
 }
 
 void SyncReplication::dictionary_erase(const CollectionBase& dict, size_t ndx, Mixed key)
 {
     Replication::dictionary_erase(dict, ndx, key);
-
-    if (select_collection(dict)) {
-        Instruction::Update instr;
-        REALM_ASSERT(key.get_type() == type_String);
-        populate_path_instr(instr, dict);
-        StringData key_value = key.get_string();
-        instr.path.push_back(m_encoder.intern_string(key_value));
-        instr.value = Instruction::Payload::Erased{};
-        instr.is_default = false;
-        emit(instr);
-    }
+    dictionary_update(dict, key, nullptr);
 }
 
 void SyncReplication::dictionary_clear(const CollectionBase& dict)
@@ -750,7 +761,25 @@ void SyncReplication::populate_path_instr(Instruction::PathInstruction& instr, c
 {
     REALM_ASSERT(key);
     // The first path entry will be the column key
-    REALM_ASSERT(path[0].is_col_key());
+    std::string field_name;
+    if (path[0].is_col_key()) {
+        auto ck = path[0].get_col_key();
+        if (table.is_additional_props_col(ck)) {
+            // We are modifying a collection nested in an additional property
+            REALM_ASSERT(path.size() > 1);
+            field_name = path[1].get_key();
+            // Erase the "__additional" part of the path
+            path.erase(path.begin());
+        }
+        else {
+            field_name = table.get_column_name(ck);
+        }
+    }
+    else {
+        // In the case of an additional property directly on an object,
+        // the first element is a string.
+        field_name = path[0].get_key();
+    }
 
     if (table.is_embedded()) {
         // For embedded objects, Obj::traverse_path() yields the top object
@@ -760,7 +789,7 @@ void SyncReplication::populate_path_instr(Instruction::PathInstruction& instr, c
         // Populate top object in the normal way.
         auto top_table = table.get_parent_group()->get_table(full_path.top_table);
 
-        full_path.path_from_top.emplace_back(table.get_column_name(path[0].get_col_key()));
+        full_path.path_from_top.emplace_back(field_name);
 
         for (auto it = path.begin() + 1; it != path.end(); ++it) {
             full_path.path_from_top.emplace_back(std::move(*it));
@@ -782,8 +811,6 @@ void SyncReplication::populate_path_instr(Instruction::PathInstruction& instr, c
         m_last_primary_key = instr.object;
     }
 
-    StringData field_name = table.get_column_name(path[0].get_col_key());
-
     if (m_last_field_name == field_name) {
         instr.field = m_last_interned_field_name;
     }
diff --git a/src/realm/sync/instruction_replication.hpp b/src/realm/sync/instruction_replication.hpp
index ff7fbd62de4..46f683dcec6 100644
--- a/src/realm/sync/instruction_replication.hpp
+++ b/src/realm/sync/instruction_replication.hpp
@@ -132,13 +132,13 @@ class SyncReplication : public Replication {
     void populate_path_instr(Instruction::PathInstruction&, const CollectionBase&);
     void populate_path_instr(Instruction::PathInstruction&, const CollectionBase&, uint32_t ndx);
 
-    void dictionary_update(const CollectionBase&, const Mixed& key, const Mixed& val);
+    void dictionary_update(const CollectionBase&, const Mixed& key, const Mixed* val);
 
     // Cache information for the purpose of avoiding excessive string comparisons / interning
     // lookups.
     const Table* m_last_table = nullptr;
     ObjKey m_last_object;
-    StringData m_last_field_name;
+    std::string m_last_field_name;
     InternString m_last_class_name;
     util::Optional<Instruction::PrimaryKey> m_last_primary_key;
     InternString m_last_interned_field_name;
diff --git a/src/realm/sync/noinst/server/server_file_access_cache.hpp b/src/realm/sync/noinst/server/server_file_access_cache.hpp
index 24af8a4adc4..51186f5b2ba 100644
--- a/src/realm/sync/noinst/server/server_file_access_cache.hpp
+++ b/src/realm/sync/noinst/server/server_file_access_cache.hpp
@@ -229,6 +229,7 @@ inline DBOptions ServerFileAccessCache::Slot::make_shared_group_options() const
         options.encryption_key = m_cache.m_encryption_key->data();
     if (m_disable_sync_to_disk)
         options.durability = DBOptions::Durability::Unsafe;
+    options.allow_flexible_schema = true;
     return options;
 }
 
diff --git a/src/realm/table.cpp b/src/realm/table.cpp
index a21820997d6..fb910594ec5 100644
--- a/src/realm/table.cpp
+++ b/src/realm/table.cpp
@@ -262,6 +262,7 @@ using namespace realm;
 using namespace realm::util;
 
 Replication* Table::g_dummy_replication = nullptr;
+static const StringData additional_properties_colum_name{"__additional"};
 
 bool TableVersions::operator==(const TableVersions& other) const
 {
@@ -634,12 +635,18 @@ void Table::init(ref_type top_ref, ArrayParent* parent, size_t ndx_in_parent, bo
     auto rot_pk_key = m_top.get_as_ref_or_tagged(top_position_for_pk_col);
     m_primary_key_col = rot_pk_key.is_tagged() ? ColKey(rot_pk_key.get_as_int()) : ColKey();
 
+    m_additional_prop_col = ColKey();
     if (m_top.size() <= top_position_for_flags) {
         m_table_type = Type::TopLevel;
     }
     else {
         uint64_t flags = m_top.get_as_ref_or_tagged(top_position_for_flags).get_as_int();
         m_table_type = Type(flags & table_type_mask);
+        if (flags & additional_prop_mask) {
+            // If we have an additional properties column, it will always be first
+            REALM_ASSERT(m_spec.m_names.size() > 0 && m_spec.m_names.get(0) == additional_properties_colum_name);
+            m_additional_prop_col = ColKey(m_spec.m_keys.get(0));
+        }
     }
     m_has_any_embedded_objects.reset();
 
@@ -2952,6 +2959,23 @@ void Table::do_set_primary_key_column(ColKey col_key)
     m_primary_key_col = col_key;
 }
 
+void Table::do_add_additional_prop_column()
+{
+    ColumnAttrMask attr;
+    attr.set(col_attr_Dictionary);
+    attr.set(col_attr_Nullable);
+    ColKey col_key = generate_col_key(col_type_Mixed, attr);
+
+    uint64_t flags = m_top.get_as_ref_or_tagged(top_position_for_flags).get_as_int();
+    flags |= additional_prop_mask;
+    m_top.set(top_position_for_flags, RefOrTagged::make_tagged(flags));
+
+    m_additional_prop_col =
+        do_insert_root_column(col_key, col_type_Mixed, additional_properties_colum_name, type_String);
+    // Be sure that it will always be first
+    REALM_ASSERT(m_additional_prop_col.get_index().val == 0);
+}
+
 bool Table::contains_unique_values(ColKey col) const
 {
     if (search_index_type(col) == IndexType::General) {
diff --git a/src/realm/table.hpp b/src/realm/table.hpp
index ac0faa5140a..278f756041c 100644
--- a/src/realm/table.hpp
+++ b/src/realm/table.hpp
@@ -93,6 +93,7 @@ class Table {
     /// <realm/object-store/object_schema.hpp>.
     enum class Type : uint8_t { TopLevel = 0, Embedded = 0x1, TopLevelAsymmetric = 0x2 };
     constexpr static uint8_t table_type_mask = 0x3;
+    constexpr static uint8_t additional_prop_mask = 0x4;
 
     /// Construct a new freestanding top-level table with static
     /// lifetime. For debugging only.
@@ -146,6 +147,15 @@ class Table {
     DataType get_dictionary_key_type(ColKey column_key) const noexcept;
     ColKey get_column_key(StringData name) const noexcept;
     ColKey get_column_key(StableIndex) const noexcept;
+    bool is_additional_props_col(ColKey ck) const
+    {
+        return ck == m_additional_prop_col;
+    }
+    ColKey get_additional_prop_col() const
+    {
+        return m_additional_prop_col;
+    }
+
     ColKeys get_column_keys() const;
     typedef util::Optional<std::pair<ConstTableRef, ColKey>> BacklinkOrigin;
     BacklinkOrigin find_backlink_origin(StringData origin_table_name, StringData origin_col_name) const noexcept;
@@ -738,6 +748,7 @@ class Table {
     Array m_opposite_column;                   // 8th slot in m_top
     std::vector<std::unique_ptr<SearchIndex>> m_index_accessors;
     ColKey m_primary_key_col;
+    ColKey m_additional_prop_col;
     Replication* const* m_repl;
     static Replication* g_dummy_replication;
     bool m_is_frozen = false;
@@ -793,6 +804,7 @@ class Table {
     ColKey find_backlink_column(ColKey origin_col_key, TableKey origin_table) const;
     ColKey find_or_add_backlink_column(ColKey origin_col_key, TableKey origin_table);
     void do_set_primary_key_column(ColKey col_key);
+    void do_add_additional_prop_column();
     void validate_column_is_unique(ColKey col_key) const;
 
     ObjKey get_next_valid_key();
@@ -954,6 +966,7 @@ class ColKeys {
 public:
     ColKeys(ConstTableRef&& t)
         : m_table(std::move(t))
+        , m_offset(m_table->get_additional_prop_col() ? 1 : 0)
     {
     }
 
@@ -964,7 +977,7 @@ class ColKeys {
 
     size_t size() const
     {
-        return m_table->get_column_count();
+        return m_table->get_column_count() - m_offset;
     }
     bool empty() const
     {
@@ -972,19 +985,20 @@ class ColKeys {
     }
     ColKey operator[](size_t p) const
     {
-        return ColKeyIterator(m_table, p).operator*();
+        return ColKeyIterator(m_table, p + m_offset).operator*();
     }
     ColKeyIterator begin() const
     {
-        return ColKeyIterator(m_table, 0);
+        return ColKeyIterator(m_table, m_offset);
     }
     ColKeyIterator end() const
     {
-        return ColKeyIterator(m_table, size());
+        return ColKeyIterator(m_table, m_table->get_column_count());
     }
 
 private:
     ConstTableRef m_table;
+    unsigned m_offset = 0;
 };
 
 // Class used to collect a chain of links when building up a Query following links.
diff --git a/src/realm/transaction.cpp b/src/realm/transaction.cpp
index 9e93875923f..a1f62970e50 100644
--- a/src/realm/transaction.cpp
+++ b/src/realm/transaction.cpp
@@ -170,6 +170,7 @@ Transaction::Transaction(DBRef _db, SlabAlloc* alloc, DB::ReadLockInfo& rli, DB:
 {
     bool writable = stage == DB::transact_Writing;
     m_transact_stage = DB::transact_Ready;
+    m_allow_additional_properties = db->m_allow_flexible_schema;
     set_transact_stage(stage);
     attach_shared(m_read_lock.m_top_ref, m_read_lock.m_file_size, writable,
                   VersionID{rli.m_version, rli.m_reader_idx});
diff --git a/test/object-store/c_api/c_api.cpp b/test/object-store/c_api/c_api.cpp
index 3d94bb112d7..59b554dfc23 100644
--- a/test/object-store/c_api/c_api.cpp
+++ b/test/object-store/c_api/c_api.cpp
@@ -2603,6 +2603,12 @@ TEST_CASE("C API - properties", "[c_api]") {
                     CHECK(strings.get() != list2.get());
                 }
 
+                SECTION("get_by_name") {
+                    auto list2 = cptr_checked(realm_get_list_by_name(obj2.get(), "strings"));
+                    CHECK(realm_equals(strings.get(), list2.get()));
+                    CHECK(strings.get() != list2.get());
+                }
+
                 SECTION("insert, then get") {
                     write([&]() {
                         CHECK(checked(realm_list_insert(strings.get(), 0, a)));
@@ -3712,6 +3718,12 @@ TEST_CASE("C API - properties", "[c_api]") {
                     CHECK(strings.get() != dict2.get());
                 }
 
+                SECTION("get by name") {
+                    auto dict2 = cptr_checked(realm_get_dictionary_by_name(obj1.get(), "nullable_string_dict"));
+                    CHECK(realm_equals(strings.get(), dict2.get()));
+                    CHECK(strings.get() != dict2.get());
+                }
+
                 SECTION("insert, then get, then erase") {
                     write([&]() {
                         bool inserted = false;
@@ -5814,6 +5826,94 @@ TEST_CASE("C API: convert", "[c_api]") {
     realm_release(realm);
 }
 
+TEST_CASE("C API: flexible schema", "[c_api]") {
+    TestFile test_file;
+    ObjectSchema object_schema = {"Foo", {{"_id", PropertyType::Int, Property::IsPrimary{true}}}};
+
+    auto config = make_config(test_file.path.c_str(), false);
+    config->schema = Schema{object_schema};
+    config->schema_version = 0;
+    config->flexible_schema = 1;
+    auto realm = realm_open(config.get());
+    realm_class_info_t class_foo;
+    bool found = false;
+    CHECK(checked(realm_find_class(realm, "Foo", &found, &class_foo)));
+    REQUIRE(found);
+
+    SECTION("Simple set/get/delete") {
+        checked(realm_begin_write(realm));
+
+        realm_value_t pk = rlm_int_val(42);
+        auto obj1 = cptr_checked(realm_object_create_with_primary_key(realm, class_foo.key, pk));
+        checked(realm_set_value_by_name(obj1.get(), "age", rlm_int_val(23)));
+        const char* prop_names[10];
+        size_t actual;
+        realm_get_additional_properties(obj1.get(), prop_names, 10, &actual);
+        REQUIRE(actual == 1);
+        CHECK(prop_names[0] == std::string_view("age"));
+        realm_has_property(obj1.get(), "age", &found);
+        REQUIRE(found);
+        realm_has_property(obj1.get(), "_id", &found);
+        REQUIRE(found);
+        realm_has_property(obj1.get(), "weight", &found);
+        REQUIRE(!found);
+
+        realm_value_t value;
+        CHECK(checked(realm_get_value_by_name(obj1.get(), "age", &value)));
+        CHECK(value.type == RLM_TYPE_INT);
+        CHECK(value.integer == 23);
+
+        checked(realm_erase_additional_property(obj1.get(), "age"));
+        realm_get_additional_properties(obj1.get(), nullptr, 0, &actual);
+        REQUIRE(actual == 0);
+        realm_commit(realm);
+    }
+
+    SECTION("Set/get nested list") {
+        checked(realm_begin_write(realm));
+
+        realm_value_t pk = rlm_int_val(42);
+        auto obj1 = cptr_checked(realm_object_create_with_primary_key(realm, class_foo.key, pk));
+        auto list = cptr_checked(realm_set_list_by_name(obj1.get(), "scores"));
+        REQUIRE(list);
+        realm_has_property(obj1.get(), "scores", &found);
+        REQUIRE(found);
+
+        realm_value_t value;
+        CHECK(checked(realm_get_value_by_name(obj1.get(), "scores", &value)));
+        CHECK(value.type == RLM_TYPE_LIST);
+
+        auto list1 = cptr_checked(realm_get_list_by_name(obj1.get(), "scores"));
+        REQUIRE(list1);
+
+        realm_commit(realm);
+    }
+
+    SECTION("Set/get nested dictionary") {
+        checked(realm_begin_write(realm));
+
+        realm_value_t pk = rlm_int_val(42);
+        auto obj1 = cptr_checked(realm_object_create_with_primary_key(realm, class_foo.key, pk));
+        auto dict = cptr_checked(realm_set_dictionary_by_name(obj1.get(), "properties"));
+        REQUIRE(dict);
+        realm_has_property(obj1.get(), "properties", &found);
+        REQUIRE(found);
+
+        realm_value_t value;
+        CHECK(checked(realm_get_value_by_name(obj1.get(), "properties", &value)));
+        CHECK(value.type == RLM_TYPE_DICTIONARY);
+
+        auto dict1 = cptr_checked(realm_get_dictionary_by_name(obj1.get(), "properties"));
+        REQUIRE(dict1);
+
+        realm_commit(realm);
+    }
+
+    realm_close(realm);
+    REQUIRE(realm_is_closed(realm));
+    realm_release(realm);
+}
+
 struct Userdata {
     std::atomic<bool> called{false};
     bool has_error;
diff --git a/test/object-store/object.cpp b/test/object-store/object.cpp
index 37ebc08ec72..bd39e428b6b 100644
--- a/test/object-store/object.cpp
+++ b/test/object-store/object.cpp
@@ -155,6 +155,80 @@ class CreatePolicyRecordingContext {
     mutable CreatePolicy last_create_policy;
 };
 
+TEST_CASE("object with flexible schema") {
+    using namespace std::string_literals;
+    _impl::RealmCoordinator::assert_no_open_realms();
+
+    InMemoryTestFile config;
+    config.automatic_change_notifications = false;
+    config.schema_mode = SchemaMode::AdditiveExplicit;
+    config.flexible_schema = true;
+    config.schema = Schema{{
+        "table",
+        {
+            {"_id", PropertyType::Int, Property::IsPrimary{true}},
+        },
+    }};
+
+    config.schema_version = 0;
+    auto r = Realm::get_shared_realm(config);
+    auto& coordinator = *_impl::RealmCoordinator::get_coordinator(config.path);
+
+    TestContext d(r);
+
+    SECTION("add_notification_callback()") {
+        auto table = r->read_group().get_table("class_table");
+        std::vector<int64_t> pks = {3, 4, 7, 9, 10, 21, 24, 34, 42, 50};
+        r->begin_transaction();
+        for (int i = 0; i < 10; ++i)
+            table->create_object_with_primary_key(pks[i]).set("value 1", i).set("value 2", i);
+        r->commit_transaction();
+
+        auto r2 = coordinator.get_realm();
+
+        CollectionChangeSet change;
+        auto obj = *table->begin();
+        Object object(r, obj);
+
+        auto write = [&](auto&& f) {
+            r->begin_transaction();
+            f();
+            r->commit_transaction();
+
+            advance_and_notify(*r);
+        };
+
+        auto require_change = [&](Object& object, std::optional<KeyPathArray> key_path_array = std::nullopt) {
+            auto token = object.add_notification_callback(
+                [&](CollectionChangeSet c) {
+                    change = c;
+                },
+                key_path_array);
+            advance_and_notify(*r);
+            return token;
+        };
+
+        SECTION("modifying the object sends a change notification") {
+            auto token = require_change(object);
+
+            write([&] {
+                obj.set("value 1", 10);
+            });
+            REQUIRE_INDICES(change.modifications, 0);
+            CHECK(change.columns.size() == 0);
+            REQUIRE(change.additional_properties.size() == 1);
+            REQUIRE_INDICES(change.additional_properties["value 1"], 0);
+
+            write([&] {
+                obj.set("value 2", 10);
+            });
+            CHECK(change.columns.size() == 0);
+            REQUIRE(change.additional_properties.size() == 1);
+            REQUIRE_INDICES(change.additional_properties["value 2"], 0);
+        }
+    }
+}
+
 TEST_CASE("object") {
     using namespace std::string_literals;
     _impl::RealmCoordinator::assert_no_open_realms();
diff --git a/test/test_lang_bind_helper.cpp b/test/test_lang_bind_helper.cpp
index 7bdd650d225..5564fa264a5 100644
--- a/test/test_lang_bind_helper.cpp
+++ b/test/test_lang_bind_helper.cpp
@@ -2151,6 +2151,10 @@ TEST_TYPES(LangBindHelper_AdvanceReadTransact_TransactLog, AdvanceReadTransact,
                 CHECK(col == link_col && obj == okey);
                 return true;
             }
+            bool modify_object(std::string&&, ObjKey)
+            {
+                return true;
+            }
             ObjKey o0, o1, okey;
             ColKey link_col, link_list_col;
         } parser(test_context);
@@ -2236,6 +2240,10 @@ TEST(LangBindHelper_AdvanceReadTransact_ErrorInObserver)
             {
                 throw ObserverError();
             }
+            bool modify_object(const std::string&, ObjKey) const
+            {
+                throw ObserverError();
+            }
         } parser(test_context);
         g->advance_read(&parser);
         CHECK(false); // Should not be reached
diff --git a/test/test_replication.cpp b/test/test_replication.cpp
index 5e0fe845dbf..822613f0009 100644
--- a/test/test_replication.cpp
+++ b/test/test_replication.cpp
@@ -299,6 +299,11 @@ struct ObjectMutationObserver : _impl::NoOpTransactionLogParser {
         CHECK(expected_modifications.erase(std::tuple(get_current_table(), obj, col)));
         return true;
     }
+    bool modify_object(std::string&&, ObjKey)
+    {
+        // CHECK(expected_modifications.erase(std::tuple(get_current_table(), obj, col)));
+        return true;
+    }
     bool remove_object(ObjKey)
     {
         return true;
diff --git a/test/test_shared.cpp b/test/test_shared.cpp
index df9ae2f778d..0b8ede76c0a 100644
--- a/test/test_shared.cpp
+++ b/test/test_shared.cpp
@@ -528,6 +528,10 @@ TEST(Shared_ReadAfterCompact)
             {
                 return true;
             }
+            bool modify_object(std::string&&, ObjKey)
+            {
+                return true;
+            }
             void parse_complete() {}
             int nb_objects = 0;
         } parser;
diff --git a/test/test_sync.cpp b/test/test_sync.cpp
index 954dca2cb7f..b4d6f89208c 100644
--- a/test/test_sync.cpp
+++ b/test/test_sync.cpp
@@ -6207,6 +6207,70 @@ TEST(Sync_DeleteCollectionInCollection)
     }
 }
 
+TEST(Sync_AdditionalProperties)
+{
+    DBOptions options;
+    options.allow_flexible_schema = true;
+    SHARED_GROUP_TEST_PATH(db_1_path);
+    SHARED_GROUP_TEST_PATH(db_2_path);
+    auto db_1 = DB::create(make_client_replication(), db_1_path, options);
+    auto db_2 = DB::create(make_client_replication(), db_2_path, options);
+
+    TEST_DIR(dir);
+    fixtures::ClientServerFixture fixture{dir, test_context};
+    fixture.start();
+
+    Session session_1 = fixture.make_session(db_1, "/test");
+    Session session_2 = fixture.make_session(db_2, "/test");
+
+    write_transaction(db_1, [&](WriteTransaction& tr) {
+        auto& g = tr.get_group();
+        auto table = g.add_table_with_primary_key("class_Table", type_Int, "id");
+        auto col_any = table->add_column(type_Mixed, "any");
+        auto foo = table->create_object_with_primary_key(123);
+        foo.set_any(col_any, "FooBar");
+        foo.set<Int>("age", 10);
+        foo.set_collection("scores", CollectionType::List);
+        auto list = foo.get_list_ptr<Mixed>("scores");
+        list->add(4.6);
+    });
+
+    session_1.wait_for_upload_complete_or_client_stopped();
+    session_2.wait_for_download_complete_or_client_stopped();
+
+    write_transaction(db_2, [&](WriteTransaction& tr) {
+        auto table = tr.get_table("class_Table");
+        CHECK_EQUAL(table->size(), 1);
+
+        auto obj = table->get_object_with_primary_key(123);
+        auto col_keys = table->get_column_keys();
+        CHECK_EQUAL(col_keys.size(), 2);
+        CHECK_EQUAL(table->get_column_name(col_keys[0]), "id");
+        CHECK_EQUAL(table->get_column_name(col_keys[1]), "any");
+        auto props = obj.get_additional_properties();
+        CHECK_EQUAL(props.size(), 2);
+        CHECK_EQUAL(obj.get<Int>("age"), 10);
+        CHECK_EQUAL(obj.get_any("any"), Mixed("FooBar"));
+        auto list = obj.get_list_ptr<Mixed>("scores");
+        CHECK_EQUAL(list->get(0), Mixed(4.6));
+        CHECK_THROW_ANY(obj.get_any("some"));
+        CHECK_THROW_ANY(obj.erase_additional_prop("any"));
+        obj.erase_additional_prop("age");
+    });
+
+    session_2.wait_for_upload_complete_or_client_stopped();
+    session_1.wait_for_download_complete_or_client_stopped();
+
+    write_transaction(db_1, [&](WriteTransaction& tr) {
+        auto table = tr.get_table("class_Table");
+        CHECK_EQUAL(table->size(), 1);
+
+        auto obj = table->get_object_with_primary_key(123);
+        auto props = obj.get_additional_properties();
+        CHECK_EQUAL(props.size(), 1);
+    });
+}
+
 TEST(Sync_NestedCollectionClear)
 {
     TEST_CLIENT_DB(db_1);