Merge pull request #743 from apache/feature/685-properties-json-serialization

Feature/685 properties json serialization
diff --git a/CHANGES.md b/CHANGES.md
index 8b30440..2720590 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -71,6 +71,8 @@
   Celix event thread.
 - Apache Celix filter now use the underlying properties value types for matching. This means that it is more important
   to add service properties with the correct type.
+- Celix C++ Exception are now defined in the `celix/exceptions.h` header file. The `celix/Exception.h`
+  and `celix/IOException.h` are removed.
 
 ## New Features
 
diff --git a/conanfile.py b/conanfile.py
index dbb748a..4e42300 100644
--- a/conanfile.py
+++ b/conanfile.py
@@ -309,7 +309,7 @@
             self.options['openssl'].shared = True
         if self.options.build_celix_dfi:
             self.options['libffi'].shared = True
-        if self.options.build_celix_dfi or self.options.build_celix_etcdlib:
+        if self.options.build_utils or self.options.build_celix_dfi or self.options.build_celix_etcdlib:
             self.options['jansson'].shared = True
 
     def requirements(self):
@@ -332,7 +332,7 @@
             self.requires("civetweb/1.16")
         if self.options.build_celix_dfi:
             self.requires("libffi/[>=3.2.1 <4.0.0]")
-        if self.options.build_celix_dfi or self.options.build_celix_etcdlib:
+        if self.options.build_utils or self.options.build_celix_dfi or self.options.build_celix_etcdlib:
             self.requires("jansson/[>=2.12 <3.0.0]")
         if self.options.build_rsa_discovery_zeroconf:
             # TODO: To be replaced with mdnsresponder/1790.80.10, resolve some problems of mdnsresponder
diff --git a/documents/README.md b/documents/README.md
index 3930498..c174acb 100644
--- a/documents/README.md
+++ b/documents/README.md
@@ -86,6 +86,7 @@
   * [Apache Celix C Patterns](c_patterns.md)
 * Utils
   * [Apache Celix Properties & Filter](properties_and_filter.md)
+  * [Apache Celix Properties Encoding](properties_encoding.md)
 * Framework 
   * [Apache Celix Bundles](bundles.md)
   * [Apache Celix Services](services.md)
diff --git a/documents/properties_encoding.md b/documents/properties_encoding.md
new file mode 100644
index 0000000..c4cdb38
--- /dev/null
+++ b/documents/properties_encoding.md
@@ -0,0 +1,333 @@
+---
+title: Apache Celix Properties Encoding
+---
+
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+   
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+# Apache Celix Properties JSON Encoding
+
+## Introduction
+
+In Apache Celix, properties represent key-value pairs, often used for configuration. While these properties are not JSON
+objects inherently, they can be encoded to and decoded from JSON for interoperability or storage. This page explains how
+Apache Celix properties are encoded to and decoded from JSON.
+
+### Encoding limitations
+
+Except for empty arrays and the double values NaN, Infinity, and -Infinity, all Apache Celix properties types can
+be encoded to JSON.
+
+The reason for the empty array limitation is that for a properties array entry the array list element type is must be
+known, this is not possible to infer from an empty JSON array. To ensure that everything this is encoded, can be decoded
+again, a properties array entry with an empty array is not encoded to JSON.
+
+The reason for the double values NaN, Infinity, and -Infinity limitation is that JSON does not support these values.
+
+### Decoding limitations
+
+When decoding JSON to Apache Celix properties, the following limitations apply:
+
+- Mixed array types are not supported. For example, an array with both strings and longs cannot be decoded to a
+  properties' entry.
+- null values are not supported, because properties does not support a null value type.
+- Empty arrays are not supported, because the array list element type must be known, this is not possible to infer from
+  an empty JSON array.
+- JSON keys that collide on the created properties' key level are not supported.
+  See [Properties Decoding](##Properties Decoding) for more information.
+
+## Properties Encoding
+
+Apache Celix properties can be encoded to JSON using the `celix_properties_save`, `celix_properties_saveToStream`
+and `celix_properties_saveToString` functions. These functions take a properties object and encode it to a JSON object
+string. The encoding can be controlled using flags and can be done in a flat or nested structure.
+
+### Properties Flat Encoding
+
+By default, the encoding is done in a flat structure, because a flat structure ensures that all keys of the properties
+can be represented in JSON format. When properties are encoded to JSON in a flat structure, the reverse operation,
+decoding JSON that has been encoded from properties, will result in the same properties (except for the previously
+mentioned limitations (empty arrays and the double values NaN, Infinity, and -Infinity)).
+
+Flat Encoding example:
+
+```C
+#include <stdio.h>
+#include <celix/properties.h>
+
+int main() {
+    celix_autoptr(celix_properties_t) props = celix_properties_create();
+
+    celix_properties_set(props, "single/strKey", "strValue");
+    celix_properties_setLong(props, "single/longKey", 42);
+    celix_properties_setDouble(props, "single/doubleKey", 2.0);
+    celix_properties_setBool(props, "single/boolKey", true);
+    celix_properties_assignVersion(props, "single/versionKey", celix_version_create(1, 2, 3, "qualifier"));
+
+    celix_array_list_t* strArr = celix_arrayList_createStringArray();
+    celix_arrayList_addString(strArr, "value1");
+    celix_arrayList_addString(strArr, "value2");
+    celix_properties_assignArrayList(props, "array/stringArr", strArr);
+
+    celix_array_list_t* longArr = celix_arrayList_createLongArray();
+    celix_arrayList_addLong(longArr, 1);
+    celix_arrayList_addLong(longArr, 2);
+    celix_properties_assignArrayList(props, "array/longArr", longArr);
+
+    celix_array_list_t* doubleArr = celix_arrayList_createDoubleArray();
+    celix_arrayList_addDouble(doubleArr, 1.0);
+    celix_arrayList_addDouble(doubleArr, 2.0);
+    celix_properties_assignArrayList(props, "array/doubleArr", doubleArr);
+
+    celix_array_list_t* boolArr = celix_arrayList_createBoolArray();
+    celix_arrayList_addBool(boolArr, true);
+    celix_arrayList_addBool(boolArr, false);
+    celix_properties_assignArrayList(props, "array/boolArr", boolArr);
+
+    celix_array_list_t* versionArr = celix_arrayList_createVersionArray();
+    celix_arrayList_assignVersion(versionArr, celix_version_create(1, 2, 3, "qualifier"));
+    celix_arrayList_assignVersion(versionArr, celix_version_create(4, 5, 6, "qualifier"));
+    celix_properties_assignArrayList(props, "array/versionArr", versionArr);
+  
+    celix_properties_saveToStream(props, stdout, CELIX_PROPERTIES_ENCODE_PRETTY);
+}
+```
+
+Will output the following JSON (order of keys can differ):
+
+```JSON
+{
+  "array/doubleArr": [
+    1.0,
+    2.0
+  ],
+  "array/boolArr": [
+    true,
+    false
+  ],
+  "single/versionKey": "version<1.2.3.qualifier>",
+  "array/longArr": [
+    1,
+    2
+  ],
+  "single/strKey": "strValue",
+  "single/doubleKey": 2.0,
+  "single/boolKey": true,
+  "array/versionArr": [
+    "version<1.2.3.qualifier>",
+    "version<4.5.6.qualifier>"
+  ],
+  "array/stringArr": [
+    "value1",
+    "value2"
+  ],
+  "single/longKey": 42
+}   
+```
+
+### Properties Nested Encoding
+
+When properties are encoded to JSON in a nested structure, the keys of the properties are used to create a nested JSON
+object. This is done by using the '/' character in the properties key to create a nested JSON objects. When encoding
+properties using a nested structure, there is a risk of key collisions. To detect key collisions, the flag
+`CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS` can be used.
+
+Nested Encoding example:
+
+```C
+#include <stdio.h>
+#include <celix/properties.h>
+
+int main() {
+    celix_autoptr(celix_properties_t) props = celix_properties_create();
+
+    celix_properties_set(props, "single/strKey", "strValue");
+    celix_properties_setLong(props, "single/longKey", 42);
+    celix_properties_setDouble(props, "single/doubleKey", 2.0);
+    celix_properties_setBool(props, "single/boolKey", true);
+    celix_properties_assignVersion(props, "single/versionKey", celix_version_create(1, 2, 3, "qualifier"));
+
+    celix_array_list_t* strArr = celix_arrayList_createStringArray();
+    celix_arrayList_addString(strArr, "value1");
+    celix_arrayList_addString(strArr, "value2");
+    celix_properties_assignArrayList(props, "array/stringArr", strArr);
+
+    celix_array_list_t* longArr = celix_arrayList_createLongArray();
+    celix_arrayList_addLong(longArr, 1);
+    celix_arrayList_addLong(longArr, 2);
+    celix_properties_assignArrayList(props, "array/longArr", longArr);
+
+    celix_array_list_t* doubleArr = celix_arrayList_createDoubleArray();
+    celix_arrayList_addDouble(doubleArr, 1.0);
+    celix_arrayList_addDouble(doubleArr, 2.0);
+    celix_properties_assignArrayList(props, "array/doubleArr", doubleArr);
+
+    celix_array_list_t* boolArr = celix_arrayList_createBoolArray();
+    celix_arrayList_addBool(boolArr, true);
+    celix_arrayList_addBool(boolArr, false);
+    celix_properties_assignArrayList(props, "array/boolArr", boolArr);
+
+    celix_array_list_t* versionArr = celix_arrayList_createVersionArray();
+    celix_arrayList_assignVersion(versionArr, celix_version_create(1, 2, 3, "qualifier"));
+    celix_arrayList_assignVersion(versionArr, celix_version_create(4, 5, 6, "qualifier"));
+    celix_properties_assignArrayList(props, "array/versionArr", versionArr);
+  
+    celix_properties_saveToStream(props, stdout, CELIX_PROPERTIES_ENCODE_PRETTY | CELIX_PROPERTIES_ENCODE_NESTED_STYLE);
+}
+```
+
+Will output the following JSON (order of keys can differ):
+
+```JSON
+{
+  "array": {
+    "doubleArr": [
+      1.0,
+      2.0
+    ],
+    "boolArr": [
+      true,
+      false
+    ],
+    "longArr": [
+      1,
+      2
+    ],
+    "versionArr": [
+      "version<1.2.3.qualifier>",
+      "version<4.5.6.qualifier>"
+    ],
+    "stringArr": [
+      "value1",
+      "value2"
+    ]
+  },
+  "single": {
+    "versionKey": "version<1.2.3.qualifier>",
+    "strKey": "strValue",
+    "doubleKey": 2.0,
+    "boolKey": true,
+    "longKey": 42
+  }
+}
+```
+
+### Encoding Flags
+
+Properties encoding flags can be used control the behavior of the encoding. The following encoding flags can be used:
+
+- `CELIX_PROPERTIES_ENCODE_PRETTY`: Flag to indicate that the encoded output should be pretty; e.g. encoded with
+  additional whitespaces, newlines and indentation. If this flag is not set, the encoded output will compact; e.g.
+  without additional whitespaces, newlines and indentation.
+
+- `CELIX_PROPERTIES_ENCODE_FLAT_STYLE`: Flag to indicate that the encoded output should be flat; e.g. all properties
+  entries are written as top level field entries.
+
+- `CELIX_PROPERTIES_ENCODE_NESTED_STYLE`: Flag to indicate that the encoded output should be nested; e.g. properties
+  entries are split on '/' and nested in JSON objects.
+
+- `CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS`: Flag to indicate that the encoding should fail if the JSON
+  representation will contain colliding keys. Note that colliding keys can only occur when using the nested encoding
+  style.
+
+- `CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS`: Flag to indicate that the encoding should fail if the JSON
+  representation will contain empty arrays.
+
+- `CELIX_PROPERTIES_ENCODE_ERROR_ON_NAN_INF`: Flag to indicate that the encoding should fail if the JSON representation
+  will contain NaN or Inf values.
+
+- `CELIX_PROPERTIES_ENCODE_STRICT`: Flag to indicate that all encode "error on" flags should be set.
+
+## Properties Decoding
+
+JSON can be decoded to an Apache Celix properties object using
+the `celix_properties_load2`, `celix_properties_loadFromStream` and `celix_properties_loadFromString2` functions. These
+functions take a JSON input and decode it to a properties object. Because properties use a flat key structure,
+decoding a nested JSON object to properties results in combining JSON object keys to a flat key structure. This can
+result in key collisions. 
+
+By default, the decoding will not fail on empty arrays, null values, empty keys, or mixed arrays and instead these JSON
+entries will be ignored. Also by default, if decoding results in a duplicate properties key, the last value will be used
+and no error will be returned.
+
+### Decoding example
+
+Given a `example.json` file with the following content:
+
+```JSON
+{
+  "counters": {
+    "counter1": 1,
+    "counter2": 2
+  },
+  "strings": {
+    "string1": "value1",
+    "string2": "value2"
+  }
+}
+```
+
+Combined with the following code:
+
+```c
+#include <stdio.h>
+
+#include <celix/properties.h>
+
+int main() {
+    celix_autoptr(celix_properties_t) props;
+    celix_status_t status = celix_properties_load2("example.json", 0, &props):
+    (void)status; //for production code check status
+    CELIX_PROPERTIES_ITERATE(props, iter) { 
+        printf("key=%s, value=%s\n", celix_properties_key(iter.key), celix_properties_value(iter.entry.value));        
+    }
+}
+```
+
+Will output the following:
+
+```
+key=counters/counter1, value=1
+key=counters/counter2, value=2
+key=strings/string1, value=value1
+key=strings/string2, value=value2
+```
+
+### Decoding Flags
+
+Properties decoding behavior can be controlled using flags. The following decoding flags can be used:
+
+- `CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES`: Flag to indicate that the decoding should fail if the input contains
+  duplicate JSON keys.
+
+- `CELIX_PROPERTIES_DECODE_ERROR_ON_COLLISIONS`: Flag to indicate that the decoding should fail if the input contains
+  entry that collide on property keys.
+
+- `CELIX_PROPERTIES_DECODE_ERROR_ON_NULL_VALUES`: Flag to indicate that the decoding should fail if the input contains
+  null values.
+
+- `CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS`: Flag to indicate that the decoding should fail if the input contains
+  empty arrays.
+
+- `CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS`: Flag to indicate that the decoding should fail if the input contains
+  empty keys.
+
+- `CELIX_PROPERTIES_DECODE_ERROR_ON_MIXED_ARRAYS`: Flag to indicate that the decoding should fail if the input contains
+  mixed arrays.
+
+- `CELIX_PROPERTIES_DECODE_STRICT`: Flag to indicate that the decoding should fail if the input contains any of the
+  decode error flags.
diff --git a/libs/error_injector/jansson/CMakeLists.txt b/libs/error_injector/jansson/CMakeLists.txt
index b536956..ce4beaa 100644
--- a/libs/error_injector/jansson/CMakeLists.txt
+++ b/libs/error_injector/jansson/CMakeLists.txt
@@ -35,5 +35,8 @@
         LINKER:--wrap,json_integer
         LINKER:--wrap,json_string
         LINKER:--wrap,json_real
+        LINKER:--wrap,json_vsprintf
+        LINKER:--wrap,json_sprintf
+        LINKER:--wrap,json_dumpf
         )
 add_library(Celix::jansson_ei ALIAS jansson_ei)
diff --git a/libs/error_injector/jansson/include/jansson_ei.h b/libs/error_injector/jansson/include/jansson_ei.h
index 60f04e4..b98bd88 100644
--- a/libs/error_injector/jansson/include/jansson_ei.h
+++ b/libs/error_injector/jansson/include/jansson_ei.h
@@ -34,6 +34,9 @@
 CELIX_EI_DECLARE(json_integer, json_t*);
 CELIX_EI_DECLARE(json_string, json_t*);
 CELIX_EI_DECLARE(json_real, json_t*);
+CELIX_EI_DECLARE(json_vsprintf,json_t*);
+CELIX_EI_DECLARE(json_sprintf, json_t*);
+CELIX_EI_DECLARE(json_dumpf, int);
 
 #ifdef __cplusplus
 }
diff --git a/libs/error_injector/jansson/src/jansson_ei.cc b/libs/error_injector/jansson/src/jansson_ei.cc
index 57033c0..98289c9 100644
--- a/libs/error_injector/jansson/src/jansson_ei.cc
+++ b/libs/error_injector/jansson/src/jansson_ei.cc
@@ -23,69 +23,94 @@
 
 extern "C" {
 
-size_t __real_json_array_size(const json_t *array);
+size_t __real_json_array_size(const json_t* array);
 CELIX_EI_DEFINE(json_array_size, size_t)
-size_t __wrap_json_array_size(const json_t *array) {
+size_t __wrap_json_array_size(const json_t* array) {
     CELIX_EI_IMPL(json_array_size);
     return __real_json_array_size(array);
 }
 
-char *__real_json_dumps(const json_t *json, size_t flags);
+char* __real_json_dumps(const json_t* json, size_t flags);
 CELIX_EI_DEFINE(json_dumps, char*)
-char *__wrap_json_dumps(const json_t *json, size_t flags) {
+char* __wrap_json_dumps(const json_t* json, size_t flags) {
     CELIX_EI_IMPL(json_dumps);
     return __real_json_dumps(json, flags);
 }
 
-json_t *__real_json_object(void);
+json_t* __real_json_object(void);
 CELIX_EI_DEFINE(json_object, json_t*)
-json_t *__wrap_json_object(void) {
+json_t* __wrap_json_object(void) {
     CELIX_EI_IMPL(json_object);
     return __real_json_object();
 }
 
-int __real_json_object_set_new(json_t *object, const char *key, json_t *value);
+int __real_json_object_set_new(json_t* object, const char* key, json_t* value);
 CELIX_EI_DEFINE(json_object_set_new, int)
-int __wrap_json_object_set_new(json_t *object, const char *key, json_t *value) {
-    json_auto_t *val = value;
+int __wrap_json_object_set_new(json_t* object, const char* key, json_t* value) {
+    json_auto_t* val = value;
     CELIX_EI_IMPL(json_object_set_new);
     return __real_json_object_set_new(object, key, celix_steal_ptr(val));
 }
 
-json_t *__real_json_array(void);
+json_t* __real_json_array(void);
 CELIX_EI_DEFINE(json_array, json_t*)
-json_t *__wrap_json_array(void) {
+json_t* __wrap_json_array(void) {
     CELIX_EI_IMPL(json_array);
     return __real_json_array();
 }
 
-int __real_json_array_append_new(json_t *array, json_t *value);
+int __real_json_array_append_new(json_t* array, json_t* value);
 CELIX_EI_DEFINE(json_array_append_new, int)
-int __wrap_json_array_append_new(json_t *array, json_t *value) {
-    json_auto_t *val = value;
+int __wrap_json_array_append_new(json_t* array, json_t* value) {
+    json_auto_t* val = value;
     CELIX_EI_IMPL(json_array_append_new);
     return __real_json_array_append_new(array, celix_steal_ptr(val));
 }
 
-json_t *__real_json_integer(json_int_t value);
+json_t* __real_json_integer(json_int_t value);
 CELIX_EI_DEFINE(json_integer, json_t*)
-json_t *__wrap_json_integer(json_int_t value) {
+json_t* __wrap_json_integer(json_int_t value) {
     CELIX_EI_IMPL(json_integer);
     return __real_json_integer(value);
 }
 
-json_t *__real_json_string(const char *value);
+json_t* __real_json_string(const char* value);
 CELIX_EI_DEFINE(json_string, json_t*)
-json_t *__wrap_json_string(const char *value) {
+json_t* __wrap_json_string(const char* value) {
     CELIX_EI_IMPL(json_string);
     return __real_json_string(value);
 }
 
-json_t *__real_json_real(double value);
+json_t* __real_json_real(double value);
 CELIX_EI_DEFINE(json_real, json_t*)
-json_t *__wrap_json_real(double value) {
+json_t* __wrap_json_real(double value) {
     CELIX_EI_IMPL(json_real);
     return __real_json_real(value);
 }
 
+json_t* __real_json_vsprintf(const char* fmt, va_list ap);
+CELIX_EI_DEFINE(json_vsprintf, json_t*)
+json_t* __wrap_json_vsprintf(const char* fmt, va_list ap) {
+    CELIX_EI_IMPL(json_vsprintf);
+    return __real_json_vsprintf(fmt, ap);
+}
+
+json_t* __real_json_sprintf(const char* fmt, ...);
+CELIX_EI_DEFINE(json_sprintf, json_t*)
+json_t* __wrap_json_sprintf(const char* fmt, ...) {
+    CELIX_EI_IMPL(json_sprintf);
+    va_list args;
+    va_start(args, fmt);
+    json_t* obj = __real_json_vsprintf(fmt, args);
+    va_end(args);
+    return obj;
+}
+
+int __real_json_dumpf(const json_t* json, FILE* output, size_t flags);
+CELIX_EI_DEFINE(json_dumpf, int)
+int __wrap_json_dumpf(const json_t* json, FILE* output, size_t flags) {
+    CELIX_EI_IMPL(json_dumpf);
+    return __real_json_dumpf(json, output, flags);
+}
+
 }
\ No newline at end of file
diff --git a/libs/framework/include/celix/FrameworkExceptions.h b/libs/framework/include/celix/FrameworkExceptions.h
index d6fd2cd..14b35a2 100644
--- a/libs/framework/include/celix/FrameworkExceptions.h
+++ b/libs/framework/include/celix/FrameworkExceptions.h
@@ -18,7 +18,7 @@
  */
 #pragma once
 
-#include "celix/Exception.h"
+#include "celix/Exceptions.h"
 
 namespace celix {
 
diff --git a/libs/framework/include/celix/ScheduledEventBuilder.h b/libs/framework/include/celix/ScheduledEventBuilder.h
index c93a93b..47cef48 100644
--- a/libs/framework/include/celix/ScheduledEventBuilder.h
+++ b/libs/framework/include/celix/ScheduledEventBuilder.h
@@ -22,8 +22,8 @@
 #include <memory>
 #include <functional>
 
+#include "celix/Exceptions.h"
 #include "celix/ScheduledEvent.h"
-#include "celix/Exception.h"
 
 namespace celix {
 
@@ -112,7 +112,7 @@
      */
     ScheduledEvent build() {
         if (!callback) {
-            throw celix::Exception{"Cannot build scheduled event without callback"}; //TODO improve error
+            throw celix::Exception{"Cannot build scheduled event without callback"};
         }
         return ScheduledEvent{ctx, name, std::move(callback), std::move(removeCallback), options};
     }
diff --git a/libs/utils/CMakeLists.txt b/libs/utils/CMakeLists.txt
index e1b4983..cec80f4 100644
--- a/libs/utils/CMakeLists.txt
+++ b/libs/utils/CMakeLists.txt
@@ -18,6 +18,7 @@
 celix_subproject(UTILS "Option to enable building the Utilities library" ON)
 if (UTILS)
     find_package(libzip REQUIRED)
+    find_package(jansson REQUIRED) #TODO add jansson dep info to build (conan) and documentation info
 
     set(MEMSTREAM_SOURCES )
     set(MEMSTREAM_INCLUDES )
@@ -29,6 +30,7 @@
             src/version.c
             src/version_range.c
             src/properties.c
+            src/properties_encoding.c
             src/utils.c
             src/filter.c
             src/celix_log_level.c
@@ -41,7 +43,7 @@
             src/celix_cleanup.c
             ${MEMSTREAM_SOURCES}
             )
-    set(UTILS_PRIVATE_DEPS libzip::zip)
+    set(UTILS_PRIVATE_DEPS libzip::zip jansson::jansson)
     set(UTILS_PUBLIC_DEPS)
 
     add_library(utils SHARED ${UTILS_SRC})
diff --git a/libs/utils/error_injector/celix_version/CMakeLists.txt b/libs/utils/error_injector/celix_version/CMakeLists.txt
index ed7aadc..81886a5 100644
--- a/libs/utils/error_injector/celix_version/CMakeLists.txt
+++ b/libs/utils/error_injector/celix_version/CMakeLists.txt
@@ -24,5 +24,6 @@
         LINKER:--wrap,celix_version_createVersionFromString
         LINKER:--wrap,celix_version_parse
         LINKER:--wrap,celix_version_copy
+        LINKER:--wrap,celix_version_toString
 )
 add_library(Celix::version_ei ALIAS version_ei)
diff --git a/libs/utils/error_injector/celix_version/include/celix_version_ei.h b/libs/utils/error_injector/celix_version/include/celix_version_ei.h
index a823185..e5d510f 100644
--- a/libs/utils/error_injector/celix_version/include/celix_version_ei.h
+++ b/libs/utils/error_injector/celix_version/include/celix_version_ei.h
@@ -31,6 +31,8 @@
 
 CELIX_EI_DECLARE(celix_version_copy, celix_version_t*);
 
+CELIX_EI_DECLARE(celix_version_toString, char*);
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/libs/utils/error_injector/celix_version/src/celix_version_ei.cc b/libs/utils/error_injector/celix_version/src/celix_version_ei.cc
index eefad50..b09339c 100644
--- a/libs/utils/error_injector/celix_version/src/celix_version_ei.cc
+++ b/libs/utils/error_injector/celix_version/src/celix_version_ei.cc
@@ -41,4 +41,11 @@
     return __real_celix_version_copy(version);
 }
 
-}
\ No newline at end of file
+char* __real_celix_version_toString(const celix_version_t* version);
+CELIX_EI_DEFINE(celix_version_toString, char*);
+char* __wrap_celix_version_toString(const celix_version_t* version) {
+    CELIX_EI_IMPL(celix_version_toString);
+    return __real_celix_version_toString(version);
+}
+
+}
diff --git a/libs/utils/gtest/CMakeLists.txt b/libs/utils/gtest/CMakeLists.txt
index d04f9e3..0d717e1 100644
--- a/libs/utils/gtest/CMakeLists.txt
+++ b/libs/utils/gtest/CMakeLists.txt
@@ -29,6 +29,7 @@
         src/CelixUtilsTestSuite.cc
         src/ConvertUtilsTestSuite.cc
         src/PropertiesTestSuite.cc
+        src/PropertiesEncodingTestSuite.cc
         src/VersionTestSuite.cc
         src/ErrTestSuite.cc
         src/ThreadsTestSuite.cc
@@ -36,6 +37,7 @@
         src/CelixUtilsAutoCleanupTestSuite.cc
         src/ArrayListTestSuite.cc
         src/DeprecatedHashmapTestSuite.cc
+        src/CxxExceptionsTestSuite.cc
 )
 
 target_link_libraries(test_utils PRIVATE utils_cut Celix::utils GTest::gtest GTest::gtest_main libzip::zip)
@@ -104,6 +106,19 @@
     add_test(NAME test_utils_celix_err_with_ei COMMAND test_utils_celix_err_with_ei)
     setup_target_for_coverage(test_utils_celix_err_with_ei SCAN_DIR ..)
 
+    #Note testing version seperated, otherwise version calls are already wrapped
+    add_executable(test_utils_version_with_ei
+            src/VersionErrorInjectionTestSuite.cc
+    )
+    target_link_libraries(test_utils_version_with_ei PRIVATE
+            utils_cut
+            Celix::malloc_ei
+            Celix::asprintf_ei
+            Celix::utils_ei
+            GTest::gtest GTest::gtest_main
+    )
+    add_test(NAME test_utils_version_with_ei COMMAND test_utils_version_with_ei)
+    setup_target_for_coverage(test_utils_version_with_ei SCAN_DIR ..)
 
     #Note testing array list seperated, otherwise array list calls are already wrapped
     add_executable(test_utils_array_list_with_ei
@@ -123,9 +138,9 @@
             src/FileUtilsErrorInjectionTestSuite.cc
             src/ConvertUtilsErrorInjectionTestSuite.cc
             src/PropertiesErrorInjectionTestSuite.cc
-            src/VersionErrorInjectionTestSuite.cc
             src/HashMapErrorInjectionTestSuite.cc
             src/FilterErrorInjectionTestSuite.cc
+            src/PropertiesEncodingErrorInjectionTestSuite.cc
     )
     target_link_libraries(test_utils_with_ei PRIVATE
             Celix::zip_ei
@@ -142,6 +157,7 @@
             Celix::long_hash_map_ei
             Celix::version_ei
             Celix::array_list_ei
+            Celix::jansson_ei
             GTest::gtest GTest::gtest_main
     )
     target_include_directories(test_utils_with_ei PRIVATE ../src) #for version_private (needs refactoring of test)
diff --git a/libs/utils/gtest/src/CelixUtilsTestSuite.cc b/libs/utils/gtest/src/CelixUtilsTestSuite.cc
index 26bef64..95e143d 100644
--- a/libs/utils/gtest/src/CelixUtilsTestSuite.cc
+++ b/libs/utils/gtest/src/CelixUtilsTestSuite.cc
@@ -315,6 +315,38 @@
     celix_utils_freeStringIfNotEqual(buffer2, out2);
 }
 
+TEST_F(UtilsTestSuite, WriteOrCreateStringGuardTest) {
+    // Given a small buffer
+    char buffer[16];
+
+    {
+        // When writing a string that fits in the buffer
+        char* str = celix_utils_writeOrCreateString(buffer, sizeof(buffer), "abc");
+
+        // Then the str is equal to the buffer (in this case no malloc was needed)
+        EXPECT_EQ(buffer, str);
+
+        // And using celix_auto with a string guard
+        celix_auto(celix_utils_string_guard_t) guard = celix_utils_stringGuard_init(buffer, str);
+
+        // Then the guard will not free the string when going out of scope
+    }
+
+    {
+        // When writing a string that does not fit in the buffer
+        char* str = celix_utils_writeOrCreateString(
+            buffer, sizeof(buffer), "abc123def456ghi789jkl012mno345pqr678stu901vwx234yz");
+
+        // Then the str is not equal to the buffer (in this case a malloc was needed)
+        EXPECT_NE(buffer, str);
+
+        // And using celix_auto with a string guard
+        celix_auto(celix_utils_string_guard_t) guard = celix_utils_stringGuard_init(buffer, str);
+
+        // Then the guard will free the string when going out of scope
+    }
+}
+
 TEST_F(UtilsTestSuite, StrDupAndStrLenTest) {
     celix_autofree char* str = celix_utils_strdup("abc");
     ASSERT_NE(nullptr, str);
diff --git a/libs/utils/gtest/src/CxxExceptionsTestSuite.cc b/libs/utils/gtest/src/CxxExceptionsTestSuite.cc
new file mode 100644
index 0000000..9ff0742
--- /dev/null
+++ b/libs/utils/gtest/src/CxxExceptionsTestSuite.cc
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include <gtest/gtest.h>
+
+#include "celix/Exceptions.h"
+
+class ExceptionsTestSuite : public ::testing::Test {
+public:
+};
+
+
+TEST_F(ExceptionsTestSuite, ThrowExceptionTest) {
+    EXPECT_THROW(celix::impl::throwException(CELIX_ILLEGAL_ARGUMENT, "Test"), celix::IllegalArgumentException);
+    try {
+        celix::impl::throwException(CELIX_ILLEGAL_ARGUMENT, "Test");
+    } catch (const celix::IllegalArgumentException& ex) {
+        EXPECT_STREQ("Test (Illegal argument)", ex.what());
+    }
+
+    EXPECT_THROW(celix::impl::throwException(CELIX_FILE_IO_EXCEPTION, "Test"), celix::IOException);
+    try {
+            celix::impl::throwException(CELIX_FILE_IO_EXCEPTION, "Test");
+    } catch (const celix::IOException& ex) {
+            EXPECT_STREQ("Test (File I/O exception)", ex.what());
+    }
+
+    //Not all celix_status_t values are mapped and in that case the default Exception is thrown
+    EXPECT_THROW(celix::impl::throwException(CELIX_FRAMEWORK_EXCEPTION, "Test"), celix::Exception);
+    try {
+        celix::impl::throwException(CELIX_FRAMEWORK_EXCEPTION, "Test");
+    } catch (const celix::Exception& ex) {
+        EXPECT_STREQ("Test (Framework exception)", ex.what());
+    }
+}
+
diff --git a/libs/utils/gtest/src/PropertiesEncodingErrorInjectionTestSuite.cc b/libs/utils/gtest/src/PropertiesEncodingErrorInjectionTestSuite.cc
new file mode 100644
index 0000000..9a5c15f
--- /dev/null
+++ b/libs/utils/gtest/src/PropertiesEncodingErrorInjectionTestSuite.cc
@@ -0,0 +1,385 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include <gtest/gtest.h>
+
+#include "celix_properties.h"
+#include "celix/Properties.h"
+
+#include "celix_err.h"
+#include "celix_array_list_ei.h"
+#include "celix_utils_ei.h"
+#include "celix_version_ei.h"
+#include "jansson_ei.h"
+#include "malloc_ei.h"
+#include "stdio_ei.h"
+
+class PropertiesEncodingErrorInjectionTestSuite : public ::testing::Test {
+  public:
+    PropertiesEncodingErrorInjectionTestSuite() = default;
+
+    ~PropertiesEncodingErrorInjectionTestSuite() override {
+        celix_ei_expect_json_object(nullptr, 0, nullptr);
+        celix_ei_expect_open_memstream(nullptr, 0, nullptr);
+        celix_ei_expect_celix_utils_writeOrCreateString(nullptr, 0, nullptr);
+        celix_ei_expect_json_object_set_new(nullptr, 0, -1);
+        celix_ei_expect_json_sprintf(nullptr, 0, nullptr);
+        celix_ei_expect_celix_version_toString(nullptr, 0, nullptr);
+        celix_ei_expect_malloc(nullptr, 0, nullptr);
+        celix_ei_expect_celix_arrayList_createWithOptions(nullptr, 0, nullptr);
+        celix_ei_expect_celix_arrayList_addString(nullptr, 0, CELIX_SUCCESS);
+        celix_err_resetErrors();
+    }
+};
+
+TEST_F(PropertiesEncodingErrorInjectionTestSuite, SaveErrorTest) {
+    //Given a dummy properties object
+    celix_autoptr(celix_properties_t) props = celix_properties_create();
+    celix_properties_set(props, "key", "value");
+
+    //When an error injected is prepared for json_object() from saveToStream
+    celix_ei_expect_json_object((void*)celix_properties_saveToStream, 0, nullptr);
+
+    //And a dummy stream is created
+    FILE* stream = fopen("/dev/null", "w");
+
+    //When I call celix_properties_saveToStream
+    celix_status_t status = celix_properties_saveToStream(props, stream, 0);
+
+    //Then I expect an error
+    EXPECT_EQ(CELIX_ENOMEM, status);
+    fclose(stream);
+
+    //When an error injected is prepared for open_memstream()n from save
+    celix_ei_expect_open_memstream((void*)celix_properties_saveToString, 0, nullptr);
+
+    //When I call celix_properties_saveToString
+    char* out;
+    status = celix_properties_saveToString(props, 0, &out);
+
+    //Then I expect an error
+    EXPECT_EQ(ENOMEM, status);
+
+    //And I expect 2 error messages in celix_err
+    EXPECT_EQ(2, celix_err_getErrorCount());
+    celix_err_printErrors(stderr, "Test Error: ", "\n");
+}
+
+TEST_F(PropertiesEncodingErrorInjectionTestSuite, FcloseErrorWhenSaveTest) {
+    //Given a dummy properties object
+    celix_autoptr(celix_properties_t) props = celix_properties_create();
+    celix_properties_set(props, "key", "value");
+
+    //When an error injected is prepared for fclose() from save
+    celix_ei_expect_fclose((void*)celix_properties_save, 0, -1);
+
+    //And I call celix_properties_save
+    auto status = celix_properties_save(props, "somefile.json", 0);
+
+    //Then I expect an error
+    EXPECT_EQ(CELIX_FILE_IO_EXCEPTION, status);
+
+    //And I expect 1 error message in celix_err
+    EXPECT_EQ(1, celix_err_getErrorCount());
+}
+
+
+TEST_F(PropertiesEncodingErrorInjectionTestSuite, EncodeErrorTest) {
+    // Given a dummy properties object
+    celix_autoptr(celix_properties_t) props = celix_properties_create();
+    celix_properties_set(props, "key.with.slash", "value");
+    celix_properties_set(props, "key-with-out-slash", "value");
+
+    // When an error injected is prepared for celix_utils_writeOrCreateString() from celix_properties_saveToString
+    celix_ei_expect_celix_utils_writeOrCreateString((void*)celix_properties_saveToString, 2, nullptr);
+
+    // And I call celix_properties_saveToString using NESTED encoding (whitebox-knowledge)
+    char* out;
+    auto status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED_STYLE, &out);
+
+    // Then I expect an error
+    EXPECT_EQ(ENOMEM, status);
+
+    // When an error injected is prepared for json_object() from celix_properties_saveToString
+    celix_ei_expect_json_object((void*)celix_properties_saveToString, 2, nullptr);
+
+    // And I call celix_properties_saveToString using NESTED encoding (whitebox-knowledge)
+    status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED_STYLE, &out);
+
+    // Then I expect an error
+    EXPECT_EQ(ENOMEM, status);
+
+    // When an error injected is prepared for json_object_set_new() from celix_properties_saveToString
+    celix_ei_expect_json_object_set_new((void*)celix_properties_saveToString, 2, -1);
+
+    // And I call celix_properties_saveToString using NESTED encoding (whitebox-knowledge)
+    status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED_STYLE, &out);
+
+    // Then I expect an error
+    EXPECT_EQ(ENOMEM, status);
+
+    // When an error injected is prepared for json_string() from celix_properties_saveToString
+    celix_ei_expect_json_string((void*)celix_properties_saveToString, 3, nullptr);
+
+    // And I call celix_properties_saveToString using NESTED encoding (whitebox-knowledge)
+    status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED_STYLE, &out);
+
+    // Then I expect an error
+    EXPECT_EQ(ENOMEM, status);
+
+    // When an error injected is prepared for json_object_set_new() from celix_properties_saveToString
+    celix_ei_expect_json_object_set_new((void*)celix_properties_saveToString, 3, -1);
+
+    // And I call celix_properties_saveToString using FLAT encoding (whitebox-knowledge)
+    status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_FLAT_STYLE, &out);
+
+    // Then I expect an error
+    EXPECT_EQ(ENOMEM, status);
+
+    // And I expect 5 error message in celix_err
+    EXPECT_EQ(5, celix_err_getErrorCount());
+    celix_err_printErrors(stderr, "Test Error: ", "\n");
+}
+
+TEST_F(PropertiesEncodingErrorInjectionTestSuite, EncodeArrayErrorTest) {
+    // Given a dummy properties object with an array
+    celix_autoptr(celix_properties_t) props = celix_properties_create();
+    auto* arr = celix_arrayList_createStringArray();
+    celix_arrayList_addString(arr, "value1");
+    celix_arrayList_addString(arr, "value2");
+    celix_properties_assignArrayList(props, "key", arr);
+
+    // When an error injected is prepared for json_array() from celix_properties_saveToString
+    celix_ei_expect_json_array((void*)celix_properties_saveToString, 4, nullptr);
+
+    // And I call celix_properties_saveToString
+    char* out;
+    auto status = celix_properties_saveToString(props, 0, &out);
+
+    // Then I expect an error
+    EXPECT_EQ(ENOMEM, status);
+
+
+    //When an error injected is prepared for json_array_append_new() from loadFromString2
+    celix_ei_expect_json_array_append_new((void*)celix_properties_saveToString, 4, -1);
+
+    //And I call celix_properties_saveToString
+    status = celix_properties_saveToString(props, 0, &out);
+
+    //Then I expect an error
+    EXPECT_EQ(ENOMEM, status);
+
+    //When an error injected is prepared for json_string() from loadFromString2
+    celix_ei_expect_json_string((void*)celix_properties_saveToString, 5, nullptr);
+
+    //And I call celix_properties_saveToString
+    status = celix_properties_saveToString(props, 0, &out);
+
+    //Then I expect an error
+    EXPECT_EQ(ENOMEM, status);
+
+    // And I expect 3 error message in celix_err
+    EXPECT_EQ(4, celix_err_getErrorCount());
+    celix_err_printErrors(stderr, "Test Error: ", "\n");
+}
+
+TEST_F(PropertiesEncodingErrorInjectionTestSuite, EncodeVersionErrorTest) {
+    // Given a dummy properties object with a version
+    celix_autoptr(celix_properties_t) props = celix_properties_create();
+    auto* version = celix_version_create(1, 2, 3, "qualifier");
+    celix_properties_assignVersion(props, "key", version);
+
+    // When an error injected is prepared for json_sprintf() from celix_properties_saveToString
+    celix_ei_expect_json_sprintf((void*)celix_properties_saveToString, 4, nullptr);
+
+    // And I call celix_properties_saveToString
+    char* out;
+    auto status = celix_properties_saveToString(props, 0, &out);
+
+    // Then I expect an error
+    EXPECT_EQ(ENOMEM, status);
+
+    // When an error injected is prepared for celix_version_toString() from celix_properties_saveToString
+    celix_ei_expect_celix_version_toString((void*)celix_properties_saveToString, 4, nullptr);
+
+    // And I call celix_properties_saveToString
+    status = celix_properties_saveToString(props, 0, &out);
+
+    // Then I expect an error
+    EXPECT_EQ(ENOMEM, status);
+
+    // And I expect 2 error message in celix_err
+    EXPECT_EQ(2, celix_err_getErrorCount());
+    celix_err_printErrors(stderr, "Test Error: ", "\n");
+}
+
+TEST_F(PropertiesEncodingErrorInjectionTestSuite, EncodeDumpfErrorTest) {
+    // Given a dummy properties object
+    celix_autoptr(celix_properties_t) props = celix_properties_create();
+    celix_properties_set(props, "key", "value");
+
+    // When an error injected is prepared for json_dumpf() from celix_properties_saveToString
+    celix_ei_expect_json_dumpf((void*)celix_properties_saveToStream, 0, -1);
+
+    // And I call celix_properties_saveToString
+    char* out;
+    auto status = celix_properties_saveToString(props, 0, &out);
+
+    // Then I expect an error
+    EXPECT_EQ(ENOMEM, status);
+
+    // And I expect 1 error message in celix_err
+    EXPECT_EQ(1, celix_err_getErrorCount());
+    celix_err_printErrors(stderr, "Test Error: ", "\n");
+}
+
+TEST_F(PropertiesEncodingErrorInjectionTestSuite, LoadErrorTest) {
+    //Given a dummy json string
+    const char* json = R"({"key":"value"})";
+
+    //When an error injected is prepared for fmemopen() from loadFromString2
+    celix_ei_expect_fmemopen((void*)celix_properties_loadFromString2, 0, nullptr);
+
+    //When I call celix_properties_loadFromString
+    celix_properties_t* props;
+    auto status = celix_properties_loadFromString2(json, 0, &props);
+
+    //Then I expect an error
+    EXPECT_EQ(ENOMEM, status);
+
+    //And I expect 1 error message in celix_err
+    EXPECT_EQ(1, celix_err_getErrorCount());
+    celix_err_printErrors(stderr, "Test Error: ", "\n");
+}
+
+TEST_F(PropertiesEncodingErrorInjectionTestSuite, DecodeErrorTest) {
+    //Given a dummy json string
+    const char* json = R"({"key":"value", "object": {"key":"value"}})";
+
+    //When an error injected is prepared for celix_properties_create()->malloc() from celix_properties_loadFromString2
+    celix_ei_expect_malloc((void*)celix_properties_loadFromString2, 3, nullptr);
+
+    //When I call celix_properties_loadFromString
+    celix_properties_t* props;
+    auto status = celix_properties_loadFromString2(json, 0, &props);
+
+    //Then I expect an error
+    EXPECT_EQ(ENOMEM, status);
+
+    //When an error injected is prepared for celix_utils_writeOrCreateString() from celix_properties_loadFromString2
+    celix_ei_expect_celix_utils_writeOrCreateString((void*)celix_properties_loadFromString2, 3, nullptr);
+
+    //When I call celix_properties_loadFromString
+    status = celix_properties_loadFromString2(json, 0, &props);
+
+    //Then I expect an error
+    EXPECT_EQ(ENOMEM, status);
+
+    //And I expect 2 error message in celix_err
+    EXPECT_EQ(2, celix_err_getErrorCount());
+    celix_err_printErrors(stderr, "Test Error: ", "\n");
+}
+
+TEST_F(PropertiesEncodingErrorInjectionTestSuite, DecodeArrayErrorTest) {
+    //Given a dummy json string
+    const char* json = R"({"key":["value1", "value2"]})";
+
+    // When an error injected is prepared for celix_arrayList_createWithOptions() from celix_properties_loadFromString2
+    celix_ei_expect_celix_arrayList_createWithOptions((void*)celix_properties_loadFromString2, 4, nullptr);
+
+    //When I call celix_properties_loadFromString
+    celix_properties_t* props;
+    auto status = celix_properties_loadFromString2(json, 0, &props);
+
+    //Then I expect an error
+    EXPECT_EQ(ENOMEM, status);
+
+    // When an error injected is prepared for celix_arrayList_addString() from celix_properties_loadFromString2
+    celix_ei_expect_celix_arrayList_addString((void*)celix_properties_loadFromString2, 4, ENOMEM);
+
+    //When I call celix_properties_loadFromString
+    status = celix_properties_loadFromString2(json, 0, &props);
+
+    //Then I expect an error
+    EXPECT_EQ(ENOMEM, status);
+
+    // And I expect 0 error message in celix_err. Note because errors are injected for celix_array_list_t, celix_err is
+    // not used
+    EXPECT_EQ(0, celix_err_getErrorCount());
+    celix_err_printErrors(stderr, "Test Error: ", "\n");
+}
+
+TEST_F(PropertiesEncodingErrorInjectionTestSuite, DecodeVersionErrorTest) {
+    // Given a dummy json version string
+    const char* json = R"({"key":"version<1.2.3.qualifier>"})";
+
+    // When an error injected is prepared for celix_utils_writeOrCreateString() from celix_properties_loadFromString2
+    celix_ei_expect_celix_utils_writeOrCreateString((void*)celix_properties_loadFromString2, 4, nullptr);
+
+    // And I call celix_properties_loadFromString
+    celix_properties_t* props;
+    auto status = celix_properties_loadFromString2(json, 0, &props);
+
+    // Then I expect an error
+    EXPECT_EQ(ENOMEM, status);
+
+    // And I expect 1 error message in celix_err
+    EXPECT_EQ(1, celix_err_getErrorCount());
+    celix_err_printErrors(stderr, "Test Error: ", "\n");
+}
+
+TEST_F(PropertiesEncodingErrorInjectionTestSuite, SaveCxxPropertiesErrorTest) {
+    //Given a dummy Properties object
+    celix::Properties props{};
+    props.set("key", "value");
+
+    //When an error injected is prepared for json_object() from saveToStream
+    celix_ei_expect_json_object((void*)celix_properties_saveToStream, 0, nullptr);
+
+    //Then saving to file throws a bad alloc exception
+    EXPECT_THROW(props.save("somefile.json"), std::bad_alloc);
+
+    //When an error injected is prepared for json_object() from saveToStream
+    celix_ei_expect_json_object((void*)celix_properties_saveToStream, 0, nullptr);
+
+    //Then saving to string throws a bad alloc exception
+    EXPECT_THROW(props.saveToString(), std::bad_alloc);
+}
+
+TEST_F(PropertiesEncodingErrorInjectionTestSuite, LoadCxxPropertiesErrorTest) {
+        //Given a dummy json string
+        const char* json = R"({"key":"value"})";
+
+        //When an error injected is prepared for malloc() from celix_properties_create
+        celix_ei_expect_malloc((void*)celix_properties_create, 0, nullptr);
+
+        //Then loading from string throws a bad alloc exception
+        EXPECT_THROW(celix::Properties::loadFromString(json), std::bad_alloc);
+
+        //When an error injected is prepared for malloc() from celix_properties_create
+        celix_ei_expect_malloc((void*)celix_properties_create, 0, nullptr);
+
+        //And an empty json file exists
+        FILE* file = fopen("empty.json", "w");
+        fprintf(file, "{}");
+        fclose(file);
+
+        //Then loading from file throws a bad alloc exception
+        EXPECT_THROW(celix::Properties::load2("empty.json"), std::bad_alloc);
+}
diff --git a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc
new file mode 100644
index 0000000..fedec72
--- /dev/null
+++ b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc
@@ -0,0 +1,1161 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include <gtest/gtest.h>
+#include <cmath>
+#include <jansson.h>
+
+#include "celix/Properties.h"
+#include "celix_err.h"
+#include "celix_properties.h"
+#include "celix_properties_private.h"
+#include "celix_stdlib_cleanup.h"
+
+
+class PropertiesSerializationTestSuite : public ::testing::Test {
+  public:
+    PropertiesSerializationTestSuite() { celix_err_resetErrors(); }
+};
+
+TEST_F(PropertiesSerializationTestSuite, SaveEmptyPropertiesTest) {
+    //Given an empty properties object
+    celix_autoptr(celix_properties_t) props = celix_properties_create();
+
+    //And an in-memory stream
+    celix_autofree char* buf = nullptr;
+    size_t bufLen = 0;
+    FILE* stream = open_memstream(&buf, &bufLen);
+
+    //When saving the properties to the stream
+    auto status = celix_properties_saveToStream(props, stream, 0);
+    ASSERT_EQ(CELIX_SUCCESS, status);
+
+    //Then the stream contains an empty JSON object
+    fclose(stream);
+    EXPECT_STREQ("{}", buf);
+}
+
+TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithSingleValuesTest) {
+        //Given a properties object with single values
+        celix_autoptr(celix_properties_t) props = celix_properties_create();
+        celix_properties_set(props, "key1", "value1");
+        celix_properties_set(props, "key2", "value2");
+        celix_properties_setLong(props, "key3", 3);
+        celix_properties_setDouble(props, "key4", 4.0);
+        celix_properties_setBool(props, "key5", true);
+        celix_properties_assignVersion(props, "key6", celix_version_create(1, 2, 3, "qualifier"));
+
+        //And an in-memory stream
+        celix_autofree char* buf = nullptr;
+        size_t bufLen = 0;
+        FILE* stream = open_memstream(&buf, &bufLen);
+
+        //When saving the properties to the stream
+        auto status = celix_properties_saveToStream(props, stream, 0);
+        ASSERT_EQ(CELIX_SUCCESS, status);
+
+        //Then the stream contains the JSON representation snippets of the properties
+        fclose(stream);
+        EXPECT_NE(nullptr, strstr(buf, R"("key1":"value1")")) << "JSON: " << buf;
+        EXPECT_NE(nullptr, strstr(buf, R"("key2":"value2")")) << "JSON: " << buf;
+        EXPECT_NE(nullptr, strstr(buf, R"("key3":3)")) << "JSON: " << buf;
+        EXPECT_NE(nullptr, strstr(buf, R"("key4":4.0)")) << "JSON: " << buf;
+        EXPECT_NE(nullptr, strstr(buf, R"("key5":true)")) << "JSON: " << buf;
+        EXPECT_NE(nullptr, strstr(buf, R"("key6":"version<1.2.3.qualifier>")")) << "JSON: " << buf;
+
+        //And the buf is a valid JSON object
+        json_error_t error;
+        json_t* root = json_loads(buf, 0, &error);
+        EXPECT_NE(nullptr, root) << "Unexpected JSON error: " << error.text;
+        json_decref(root);
+}
+
+TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithNaNAndInfValuesTest) {
+    //Given a NAN, INF and -INF value
+    auto keys = {"NAN", "INF", "-INF"};
+    for (const auto& key : keys) {
+        //For every value
+
+        //Given a properties object with a NAN, INF or -INF value
+        celix_autoptr(celix_properties_t) props = celix_properties_create();
+        celix_properties_setDouble(props, key, strtod(key, nullptr));
+
+        // Then saving the properties to a string succeeds, but value is not added to the JSON (because JSON does not
+        // support NAN, INF and -INF)
+        celix_autofree char* output;
+        auto status = celix_properties_saveToString(props, 0, &output);
+        ASSERT_EQ(CELIX_SUCCESS, status);
+        EXPECT_STREQ("{}", output);
+
+        //And saving the properties to a string with the flag CELIX_PROPERTIES_ENCODE_ERROR_ON_NAN_INF fails
+        celix_err_resetErrors();
+        char* output2;
+        status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_ERROR_ON_NAN_INF, &output2);
+        EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status);
+
+        //And an error msg is added to celix_err
+        EXPECT_EQ(1, celix_err_getErrorCount());
+    }
+}
+
+TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithArrayListsContainingNaNAndInfValueTest) {
+    auto keys = {"NAN", "INF", "-INF"};
+    for (const auto& key : keys) {
+        celix_autoptr(celix_properties_t) props = celix_properties_create();
+        celix_autoptr(celix_array_list_t) list = celix_arrayList_createDoubleArray();
+        celix_arrayList_addDouble(list, strtod(key, nullptr));
+        celix_properties_assignArrayList(props, key, celix_steal_ptr(list));
+
+        // Then saving the properties to a string succeeds, but value is not added to the JSON (because JSON does not
+        // support NAN, INF and -INF)
+        celix_autofree char* output;
+        auto status = celix_properties_saveToString(props, 0, &output);
+        ASSERT_EQ(CELIX_SUCCESS, status);
+        EXPECT_STREQ("{}", output);
+
+        //And saving the properties to a string with the flag CELIX_PROPERTIES_ENCODE_ERROR_ON_NAN_INF fails
+        celix_err_resetErrors();
+        char* output2;
+        status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_ERROR_ON_NAN_INF, &output2);
+        EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status);
+        //And an error msg is added to celix_err
+        EXPECT_EQ(2, celix_err_getErrorCount());
+
+        celix_err_resetErrors();
+        char* output3;
+        status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS, &output3);
+        EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status);
+        EXPECT_EQ(1, celix_err_getErrorCount());
+    }
+}
+
+TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithArrayListsTest) {
+    // Given a properties object with array list values
+    celix_autoptr(celix_properties_t) props = celix_properties_create();
+
+    celix_array_list_t* list1 = celix_arrayList_createStringArray();
+    celix_arrayList_addString(list1, "value1");
+    celix_arrayList_addString(list1, "value2");
+    celix_properties_assignArrayList(props, "key1", list1);
+
+    celix_array_list_t* list2 = celix_arrayList_createLongArray();
+    celix_arrayList_addLong(list2, 1);
+    celix_arrayList_addLong(list2, 2);
+    celix_properties_assignArrayList(props, "key2", list2);
+
+    celix_array_list_t* list3 = celix_arrayList_createDoubleArray();
+    celix_arrayList_addDouble(list3, 1.0);
+    celix_arrayList_addDouble(list3, 2.0);
+    celix_properties_assignArrayList(props, "key3", list3);
+
+    celix_array_list_t* list4 = celix_arrayList_createBoolArray();
+    celix_arrayList_addBool(list4, true);
+    celix_arrayList_addBool(list4, false);
+    celix_properties_assignArrayList(props, "key4", list4);
+
+    celix_array_list_t* list5 = celix_arrayList_createVersionArray();
+    celix_arrayList_assignVersion(list5, celix_version_create(1, 2, 3, "qualifier"));
+    celix_arrayList_assignVersion(list5, celix_version_create(4, 5, 6, "qualifier"));
+    celix_properties_assignArrayList(props, "key5", list5);
+
+    // And an in-memory stream
+    celix_autofree char* buf = nullptr;
+    size_t bufLen = 0;
+    FILE* stream = open_memstream(&buf, &bufLen);
+
+    // When saving the properties to the stream
+    auto status = celix_properties_saveToStream(props, stream, 0);
+    ASSERT_EQ(CELIX_SUCCESS, status);
+
+    // Then the stream contains the JSON representation snippets of the properties
+    fclose(stream);
+    EXPECT_NE(nullptr, strstr(buf, R"("key1":["value1","value2"])")) << "JSON: " << buf;
+    EXPECT_NE(nullptr, strstr(buf, R"("key2":[1,2])")) << "JSON: " << buf;
+    EXPECT_NE(nullptr, strstr(buf, R"("key3":[1.0,2.0])")) << "JSON: " << buf;
+    EXPECT_NE(nullptr, strstr(buf, R"("key4":[true,false])")) << "JSON: " << buf;
+    EXPECT_NE(nullptr, strstr(buf, R"("key5":["version<1.2.3.qualifier>","version<4.5.6.qualifier>"])"))
+        << "JSON: " << buf;
+
+    // And the buf is a valid JSON object
+    json_error_t error;
+    json_t* root = json_loads(buf, 0, &error);
+    EXPECT_NE(nullptr, root) << "Unexpected JSON error: " << error.text;
+    json_decref(root);
+}
+
+
+TEST_F(PropertiesSerializationTestSuite, SaveEmptyArrayTest) {
+    //Given a properties object with an empty array list of with el types string, long, double, bool, version
+    celix_autoptr(celix_properties_t) props = celix_properties_create();
+    celix_properties_assignArrayList(props, "key1", celix_arrayList_createStringArray());
+    celix_properties_assignArrayList(props, "key2", celix_arrayList_createLongArray());
+    celix_properties_assignArrayList(props, "key3", celix_arrayList_createDoubleArray());
+    celix_properties_assignArrayList(props, "key4", celix_arrayList_createBoolArray());
+    celix_properties_assignArrayList(props, "key5", celix_arrayList_createVersionArray());
+    EXPECT_EQ(5, celix_properties_size(props));
+
+    //When saving the properties to a string
+    celix_autofree char* output1;
+    auto status = celix_properties_saveToString(props, 0, &output1);
+
+    //Then the save went ok
+    ASSERT_EQ(CELIX_SUCCESS, status);
+
+    //And the output contains an empty JSON object, because empty arrays are treated as unset
+    EXPECT_STREQ("{}", output1);
+
+    //When saving the properties to a string with an error on  empty array flag
+    char* output2;
+    status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS, &output2);
+
+    //Then the save fails, because the empty array generates an error
+    ASSERT_EQ(CELIX_ILLEGAL_ARGUMENT, status);
+
+    //And at least one error message is added to celix_err
+    EXPECT_GE(celix_err_getErrorCount(), 1);
+    celix_err_printErrors(stderr, "Test Error: ", "\n");
+}
+
+TEST_F(PropertiesSerializationTestSuite, SaveEmptyKeyTest) {
+    celix_autoptr(celix_properties_t) props = celix_properties_create();
+    celix_properties_setString(props, "", "value");
+
+    celix_autofree char* output1;
+    auto status = celix_properties_saveToString(props, 0, &output1);
+    ASSERT_EQ(CELIX_SUCCESS, status);
+
+    celix_autoptr(celix_properties_t) prop2 = nullptr;
+    status = celix_properties_loadFromString2(output1, 0, &prop2);
+    ASSERT_EQ(CELIX_SUCCESS, status);
+
+    ASSERT_TRUE(celix_properties_equals(props, prop2));
+}
+
+TEST_F(PropertiesSerializationTestSuite, SaveJSONPathKeysTest) {
+    //Given a properties object with jpath keys
+    celix_autoptr(celix_properties_t) props = celix_properties_create();
+    celix_properties_set(props, "key1", "value1");
+    celix_properties_set(props, "key2", "value2");
+    celix_properties_set(props, "object1.key3", "value3");
+    celix_properties_set(props, "object1.key4", "value4");
+    celix_properties_set(props, "object2.key5", "value5");
+    celix_properties_set(props, "object3.object4.key6", "value6");
+
+    //And an in-memory stream
+    celix_autofree char* output;
+
+    //When saving the properties to the stream
+    auto status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED_STYLE, &output);
+    ASSERT_EQ(CELIX_SUCCESS, status);
+
+    //Then the stream contains the JSON representation snippets of the properties
+    EXPECT_NE(nullptr, strstr(output, R"("key1":"value1")")) << "JSON: " << output;
+    EXPECT_NE(nullptr, strstr(output, R"("key2":"value2")")) << "JSON: " << output;
+    EXPECT_NE(nullptr, strstr(output, R"("object1":{"key3":"value3","key4":"value4"})")) << "JSON: " << output;
+    EXPECT_NE(nullptr, strstr(output, R"("object2":{"key5":"value5"})")) << "JSON: " << output;
+    EXPECT_NE(nullptr, strstr(output, R"("object3":{"object4":{"key6":"value6"}})")) << "JSON: " << output;
+
+    //And the buf is a valid JSON object
+    json_error_t error;
+    json_auto_t* root = json_loads(output, 0, &error);
+    EXPECT_NE(nullptr, root) << "Unexpected JSON error: " << error.text;
+}
+
+TEST_F(PropertiesSerializationTestSuite, SaveJPathKeysWithCollisionTest) {
+    // note this tests depends on the key iteration order for properties and
+    // properties key order is based on hash order of the keys, so this test can change if the string hash map
+    // implementation changes.
+
+    //Given a properties object with jpath keys that collide
+    celix_autoptr(celix_properties_t) props = celix_properties_create();
+    celix_properties_set(props, "key1.key2.key3", "value1");
+    celix_properties_set(props, "key1.key2", "value2"); //collision with object "key1/key2/key3" -> overwrite
+    celix_properties_set(props, "key4.key5.key6.key7", "value4");
+    celix_properties_set(props, "key4.key5.key6", "value3"); //collision with field "key4/key5/key6/key7" -> overwrite
+
+    //When saving the properties to a string
+    celix_autofree char* output = nullptr;
+    auto status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED_STYLE, &output);
+    ASSERT_EQ(CELIX_SUCCESS, status);
+
+    //Then the stream contains the JSON representation of the properties with the collisions resolved
+    EXPECT_NE(nullptr, strstr(output, R"({"key1":{"key2":"value2"},"key4":{"key5":{"key6":"value3"}}})"))
+        << "JSON: " << output;
+
+    //And the buf is a valid JSON object
+    json_error_t error;
+    json_t* root = json_loads(output, 0, &error);
+    EXPECT_NE(nullptr, root) << "Unexpected JSON error: " << error.text;
+    json_decref(root);
+}
+
+TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithNestedEndErrorOnCollisionsFlagsTest) {
+    celix_autoptr(celix_properties_t) props = celix_properties_create();
+    celix_properties_set(props, "key1", "value1");
+    celix_properties_set(props, "key2", "value2");
+    celix_properties_set(props, "object1.key3", "value3");
+    celix_properties_set(props, "object1.key4", "value4");
+    celix_properties_set(props, "object2.key5", "value5");
+    celix_properties_set(props, "object3.object4.key6", "value6");
+
+    // And an in-memory stream
+    celix_autofree char* output;
+
+    // When saving the properties to the stream
+    auto status = celix_properties_saveToString(
+        props, CELIX_PROPERTIES_ENCODE_NESTED_STYLE | CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS, &output);
+    ASSERT_EQ(CELIX_SUCCESS, status);
+}
+
+TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithKeyNamesWithDotsTest) {
+    //Given a properties set with key names with dots
+    celix_autoptr(celix_properties_t) props = celix_properties_create();
+    celix_properties_set(props, "a.key.name.with.dots", "value1");
+    celix_properties_set(props, ".keyThatStartsWithDot", "value3");
+    celix_properties_set(props, "keyThatEndsWithDot.", "value5");
+    celix_properties_set(props, "keyThatEndsWithDoubleDots..", "value6");
+    celix_properties_set(props, "key..With..Double..Dots", "value7");
+    celix_properties_set(props, "object.keyThatEndsWithDot.", "value8");
+    celix_properties_set(props, "object.keyThatEndsWithDoubleDots..", "value9");
+    celix_properties_set(props, "object.key..With..Double..Dots", "value10");
+
+
+    //When saving the properties to a string
+    celix_autofree char* output;
+    auto status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED_STYLE, &output);
+    ASSERT_EQ(CELIX_SUCCESS, status);
+
+    //Then the out contains the JSON representation snippets of the properties
+    EXPECT_NE(nullptr, strstr(output, R"("a":{"key":{"name":{"with":{"dots":"value1"}}}})")) << "JSON: " << output;
+    EXPECT_NE(nullptr, strstr(output, R"("keyThatStartsWithDot":"value3")")) << "JSON: " << output;
+    EXPECT_NE(nullptr, strstr(output, R"("":"value5")")) << "JSON: " << output;
+    EXPECT_NE(nullptr, strstr(output, R"("":"value6")")) << "JSON: " << output;
+    EXPECT_NE(nullptr, strstr(output, R"("Dots":"value7")")) << "JSON: " << output;
+    EXPECT_NE(nullptr, strstr(output, R"("":"value8")")) << "JSON: " << output;
+    EXPECT_NE(nullptr, strstr(output, R"("":"value9")")) << "JSON: " << output;
+    EXPECT_NE(nullptr, strstr(output, R"("Dots":"value10")")) << "JSON: " << output;
+
+    //And the output is a valid JSON object
+    json_error_t error;
+    json_t* root = json_loads(output, 0, &error);
+    EXPECT_NE(nullptr, root) << "Unexpected JSON error: " << error.text;
+
+
+    //And the structure for (e.g.) value10 is correct
+    json_t* node = json_object_get(root, "object");
+    ASSERT_NE(nullptr, node);
+    ASSERT_TRUE(json_is_object(node));
+    node = json_object_get(node, "key");
+    ASSERT_NE(nullptr, node);
+    ASSERT_TRUE(json_is_object(node));
+    node = json_object_get(node, "");
+    ASSERT_NE(nullptr, node);
+    ASSERT_TRUE(json_is_object(node));
+    node = json_object_get(node, "With");
+    ASSERT_NE(nullptr, node);
+    ASSERT_TRUE(json_is_object(node));
+    node = json_object_get(node, "");
+    ASSERT_NE(nullptr, node);
+    ASSERT_TRUE(json_is_object(node));
+    node = json_object_get(node, "Double");
+    ASSERT_NE(nullptr, node);
+    ASSERT_TRUE(json_is_object(node));
+    node = json_object_get(node, "");
+    ASSERT_NE(nullptr, node);
+    ASSERT_TRUE(json_is_object(node));
+    node = json_object_get(node, "Dots");
+    ASSERT_NE(nullptr, node);
+    ASSERT_TRUE(json_is_string(node));
+    EXPECT_STREQ("value10", json_string_value(node));
+
+    json_decref(root);
+}
+
+TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithKeyCollision) {
+    // note this tests depends on the key iteration order for properties and
+    // properties key order is based on hash order of the keys, so this test can change if the string hash map
+    // implementation changes.
+
+    //Given a properties that contains keys that will collide with an existing JSON object
+    celix_autoptr(celix_properties_t) props = celix_properties_create();
+    celix_properties_set(props, "key1.key2.key3", "value1");
+    celix_properties_set(props, "key1.key2", "value2"); //collision with object "key1.key2" -> overwrite
+
+    //When saving the properties to a string
+    celix_autofree char* output1;
+    auto status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED_STYLE, &output1);
+
+    //Then the save succeeds
+    ASSERT_EQ(CELIX_SUCCESS, status);
+
+    // And both keys are serialized (one as a flat key) (flat key name is whitebox knowledge)
+    EXPECT_NE(nullptr, strstr(output1, R"({"key1":{"key2":"value2"}})")) << "JSON: " << output1;
+
+    //When saving the properties to a string with the error on key collision flag
+    char* output2;
+    status = celix_properties_saveToString(
+        props, CELIX_PROPERTIES_ENCODE_NESTED_STYLE | CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS, &output2);
+
+    //Then the save fails, because the keys collide
+    ASSERT_EQ(CELIX_ILLEGAL_ARGUMENT, status);
+
+    //And at least one error message is added to celix_err
+    EXPECT_GE(celix_err_getErrorCount(), 1);
+    celix_err_printErrors(stderr, "Test Error: ", "\n");
+}
+
+TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithAndWithoutStrictFlagTest) {
+    //Given a properties set with an empty array list
+    celix_autoptr(celix_properties_t) props = celix_properties_create();
+    auto* list = celix_arrayList_createStringArray();
+    celix_properties_assignArrayList(props, "key1", list);
+
+    //When saving the properties to a string without the strict flag
+    celix_autofree char* output;
+    auto status = celix_properties_saveToString(props, 0, &output);
+
+    //Then the save succeeds
+    ASSERT_EQ(CELIX_SUCCESS, status);
+
+    //When saving the properties to a string with the strict flag
+    char* output2;
+    status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_STRICT, &output2);
+
+    //Then the save fails, because the empty array generates an error
+    ASSERT_EQ(CELIX_ILLEGAL_ARGUMENT, status);
+
+    //And at least one error message is added to celix_err
+    EXPECT_GE(celix_err_getErrorCount(), 1);
+    celix_err_printErrors(stderr, "Test Error: ", "\n");
+}
+
+TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithPrettyPrintTest) {
+    //Given a properties set with 2 keys
+    celix_autoptr(celix_properties_t) props = celix_properties_create();
+    celix_properties_set(props, "key1", "value1");
+    celix_properties_set(props, "key2", "value2");
+
+    //When saving the properties to a string with pretty print
+    celix_autofree char* output;
+    auto status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_PRETTY, &output);
+
+    //Then the save succeeds
+    ASSERT_EQ(CELIX_SUCCESS, status);
+
+    // And the output contains the JSON representation snippets of the properties with pretty print (2 indent spaces and
+    // newlines)
+    auto* expected = "{\n  \"key2\": \"value2\",\n  \"key1\": \"value1\"\n}";
+    EXPECT_STREQ(expected, output);
+}
+
+TEST_F(PropertiesSerializationTestSuite, SaveWithInvalidStreamTest) {
+    celix_autoptr(celix_properties_t) properties = celix_properties_create();
+    celix_properties_set(properties, "key", "value");
+
+    // Saving properties with invalid stream will fail
+    auto status = celix_properties_save(properties, "/non-existing/no/rights/file.json", 0);
+    EXPECT_EQ(CELIX_FILE_IO_EXCEPTION, status);
+    EXPECT_EQ(1, celix_err_getErrorCount());
+
+    auto* readStream = fopen("/dev/null", "r");
+    status = celix_properties_saveToStream(properties, readStream, 0);
+    EXPECT_EQ(CELIX_FILE_IO_EXCEPTION, status);
+    EXPECT_EQ(2, celix_err_getErrorCount());
+    fclose(readStream);
+
+    celix_err_printErrors(stderr, "Test Error: ", "\n");
+}
+
+TEST_F(PropertiesSerializationTestSuite, SaveCxxPropertiesTest) {
+    // Given a C++ Properties object with 2 keys
+    celix::Properties props{};
+    props.set("key1", "value1");
+    props.set("key2", 42);
+    props.setVector("key3", std::vector<bool>{}); // empty vector
+
+    // When saving the properties to a string
+    std::string result = props.saveToString();
+
+    // Then the result contains the JSON representation snippets of the properties
+    EXPECT_NE(std::string::npos, result.find("\"key1\":\"value1\""));
+    EXPECT_NE(std::string::npos, result.find("\"key2\":42"));
+
+    // When saving the properties to a string using a flat style
+    std::string result2 = props.saveToString(celix::Properties::EncodingFlags::FlatStyle);
+
+    //The result is equals to a default save
+    EXPECT_EQ(result, result2);
+
+    // When saving the properties to a string using an errors on duplicate key flag
+    EXPECT_THROW(props.saveToString(celix::Properties::EncodingFlags::Strict),
+                 celix::IllegalArgumentException);
+
+    // When saving the properties to a string using combined flags
+    EXPECT_THROW(props.saveToString(
+                     celix::Properties::EncodingFlags::Pretty | celix::Properties::EncodingFlags::ErrorOnEmptyArrays |
+                     celix::Properties::EncodingFlags::ErrorOnCollisions |
+                     celix::Properties::EncodingFlags::ErrorOnNanInf | celix::Properties::EncodingFlags::NestedStyle),
+                 celix::IllegalArgumentException);
+
+    // When saving the properties to an invalid filename location
+    EXPECT_THROW(props.save("/non-existing/no/rights/file.json"),
+                     celix::IOException);
+}
+
+TEST_F(PropertiesSerializationTestSuite, LoadEmptyPropertiesTest) {
+    //Given an empty JSON object
+    const char* json = "{}";
+
+    //When loading the properties from the stream
+    celix_autoptr(celix_properties_t) props = nullptr;
+    auto status = celix_properties_loadFromString2(json, 0, &props);
+    ASSERT_EQ(CELIX_SUCCESS, status);
+
+    //Then the properties object is empty
+    EXPECT_EQ(0, celix_properties_size(props));
+}
+
+TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithSingleValuesTest) {
+    //Given a JSON object with single values for types string, long, double, bool and version
+    const char* jsonInput = R"({
+        "strKey":"strValue",
+        "longKey":42,
+        "doubleKey":2.0,
+        "boolKey":true,
+        "versionKey":"version<1.2.3.qualifier>"
+    })";
+
+    //And a stream with the JSON object
+    FILE* stream = fmemopen((void*)jsonInput, strlen(jsonInput), "r");
+
+    //When loading the properties from the stream
+    celix_autoptr(celix_properties_t) props = nullptr;
+    auto status = celix_properties_loadFromStream(stream, 0, &props);
+    ASSERT_EQ(CELIX_SUCCESS, status);
+
+    //Then the properties object contains the single values
+    EXPECT_EQ(5, celix_properties_size(props));
+    EXPECT_STREQ("strValue", celix_properties_getString(props, "strKey"));
+    EXPECT_EQ(42, celix_properties_getLong(props, "longKey", -1));
+    EXPECT_DOUBLE_EQ(2.0, celix_properties_getDouble(props, "doubleKey", NAN));
+    EXPECT_TRUE(celix_properties_getBool(props, "boolKey", false));
+    auto* v = celix_properties_getVersion(props, "versionKey");
+    ASSERT_NE(nullptr, v);
+    celix_autofree char* vStr = celix_version_toString(v);
+    EXPECT_STREQ("1.2.3.qualifier", vStr);
+}
+
+TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithArrayListsTest) {
+    //Given a JSON object with array values for types string, long, double, bool and version
+    const char* jsonInput = R"({
+        "strArr":["value1","value2"],
+        "intArr":[1,2],
+        "realArr":[1.0,2.0],
+        "boolArr":[true,false],
+        "versionArr":["version<1.2.3.qualifier>","version<4.5.6.qualifier>"],
+        "mixedRealAndIntArr1":[1,2.0,2,3.0],
+        "mixedRealAndIntArr2":[1.0,2,2.0,3]
+    })";
+
+    //And a stream with the JSON object
+    FILE* stream = fmemopen((void*)jsonInput, strlen(jsonInput), "r");
+
+    //When loading the properties from the stream
+    celix_autoptr(celix_properties_t) props = nullptr;
+    auto status = celix_properties_loadFromStream(stream, 0, &props);
+    ASSERT_EQ(CELIX_SUCCESS, status);
+
+    //Then the properties object contains the array values
+    EXPECT_EQ(7, celix_properties_size(props));
+
+    //And the string array is correctly loaded
+    auto* strArr = celix_properties_getArrayList(props, "strArr");
+    ASSERT_NE(nullptr, strArr);
+    EXPECT_EQ(CELIX_ARRAY_LIST_ELEMENT_TYPE_STRING, celix_arrayList_getElementType(strArr));
+    EXPECT_EQ(2, celix_arrayList_size(strArr));
+    EXPECT_STREQ("value1", celix_arrayList_getString(strArr, 0));
+    EXPECT_STREQ("value2", celix_arrayList_getString(strArr, 1));
+
+    //And the long array is correctly loaded
+    auto* intArr = celix_properties_getArrayList(props, "intArr");
+    ASSERT_NE(nullptr, intArr);
+    EXPECT_EQ(CELIX_ARRAY_LIST_ELEMENT_TYPE_LONG, celix_arrayList_getElementType(intArr));
+    EXPECT_EQ(2, celix_arrayList_size(intArr));
+    EXPECT_EQ(1, celix_arrayList_getLong(intArr, 0));
+    EXPECT_EQ(2, celix_arrayList_getLong(intArr, 1));
+
+    //And the double array is correctly loaded
+    auto* realArr = celix_properties_getArrayList(props, "realArr");
+    ASSERT_NE(nullptr, realArr);
+    EXPECT_EQ(CELIX_ARRAY_LIST_ELEMENT_TYPE_DOUBLE, celix_arrayList_getElementType(realArr));
+    EXPECT_EQ(2, celix_arrayList_size(realArr));
+    EXPECT_DOUBLE_EQ(1.0, celix_arrayList_getDouble(realArr, 0));
+    EXPECT_DOUBLE_EQ(2.0, celix_arrayList_getDouble(realArr, 1));
+
+    //And the bool array is correctly loaded
+    auto* boolArr = celix_properties_getArrayList(props, "boolArr");
+    ASSERT_NE(nullptr, boolArr);
+    EXPECT_EQ(CELIX_ARRAY_LIST_ELEMENT_TYPE_BOOL, celix_arrayList_getElementType(boolArr));
+    EXPECT_EQ(2, celix_arrayList_size(boolArr));
+    EXPECT_TRUE(celix_arrayList_getBool(boolArr, 0));
+    EXPECT_FALSE(celix_arrayList_getBool(boolArr, 1));
+
+    //And the version array is correctly loaded
+    auto* versionArr = celix_properties_getArrayList(props, "versionArr");
+    ASSERT_NE(nullptr, versionArr);
+    EXPECT_EQ(CELIX_ARRAY_LIST_ELEMENT_TYPE_VERSION, celix_arrayList_getElementType(versionArr));
+    EXPECT_EQ(2, celix_arrayList_size(versionArr));
+    auto* v1 = celix_arrayList_getVersion(versionArr, 0);
+    ASSERT_NE(nullptr, v1);
+    celix_autofree char* v1Str = celix_version_toString(v1);
+    EXPECT_STREQ("1.2.3.qualifier", v1Str);
+    auto* v2 = celix_arrayList_getVersion(versionArr, 1);
+    ASSERT_NE(nullptr, v2);
+    celix_autofree char* v2Str = celix_version_toString(v2);
+    EXPECT_STREQ("4.5.6.qualifier", v2Str);
+
+    //And the mixed json real and int arrays are correctly loaded as double arrays
+    auto* mixedRealAndIntArr1 = celix_properties_getArrayList(props, "mixedRealAndIntArr1");
+    ASSERT_NE(nullptr, mixedRealAndIntArr1);
+    EXPECT_EQ(CELIX_ARRAY_LIST_ELEMENT_TYPE_DOUBLE, celix_arrayList_getElementType(mixedRealAndIntArr1));
+    EXPECT_EQ(4, celix_arrayList_size(mixedRealAndIntArr1));
+    EXPECT_DOUBLE_EQ(1.0, celix_arrayList_getDouble(mixedRealAndIntArr1, 0));
+    EXPECT_DOUBLE_EQ(2.0, celix_arrayList_getDouble(mixedRealAndIntArr1, 1));
+    EXPECT_DOUBLE_EQ(2.0, celix_arrayList_getDouble(mixedRealAndIntArr1, 2));
+    EXPECT_DOUBLE_EQ(3.0, celix_arrayList_getDouble(mixedRealAndIntArr1, 3));
+
+    auto* mixedRealAndIntArr2 = celix_properties_getArrayList(props, "mixedRealAndIntArr2");
+    ASSERT_NE(nullptr, mixedRealAndIntArr2);
+    EXPECT_EQ(CELIX_ARRAY_LIST_ELEMENT_TYPE_DOUBLE, celix_arrayList_getElementType(mixedRealAndIntArr2));
+    EXPECT_EQ(4, celix_arrayList_size(mixedRealAndIntArr2));
+    EXPECT_DOUBLE_EQ(1.0, celix_arrayList_getDouble(mixedRealAndIntArr2, 0));
+    EXPECT_DOUBLE_EQ(2.0, celix_arrayList_getDouble(mixedRealAndIntArr2, 1));
+    EXPECT_DOUBLE_EQ(2.0, celix_arrayList_getDouble(mixedRealAndIntArr2, 2));
+    EXPECT_DOUBLE_EQ(3.0, celix_arrayList_getDouble(mixedRealAndIntArr2, 3));
+}
+
+TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithInvalidInputTest) {
+    auto invalidInputs = {
+        R"({)",                            // invalid JSON (caught by jansson)
+        R"([])",                           // unsupported JSON (top level array not supported)
+        R"(42)",                           // invalid JSON (caught by jansson)
+    };
+    for (auto& invalidInput: invalidInputs) {
+        //Given an invalid JSON object
+        FILE* stream = fmemopen((void*)invalidInput, strlen(invalidInput), "r");
+
+        //When loading the properties from the stream
+        celix_autoptr(celix_properties_t) props = nullptr;
+        auto status = celix_properties_loadFromStream(stream, 0, &props);
+
+        //Then loading fails
+        EXPECT_NE(CELIX_SUCCESS, status);
+
+        //And at least one error message is added to celix_err
+        EXPECT_GE(celix_err_getErrorCount(), 1);
+        celix_err_printErrors(stderr, "Test Error: ", "\n");
+
+        fclose(stream);
+    }
+}
+
+TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithEmptyArrayTest) {
+    //Given a JSON object with an empty array
+    auto* inputJSON = R"({"key1":[]})";
+
+    //When loading the properties from string
+    celix_autoptr(celix_properties_t) props = nullptr;
+    auto status = celix_properties_loadFromString2(inputJSON, 0, &props);
+
+    //Then loading succeeds
+    ASSERT_EQ(CELIX_SUCCESS, status);
+
+    //And the properties object is empty, because empty arrays are treated as unset
+    EXPECT_EQ(0, celix_properties_size(props));
+
+    //When loading the properties from string with a strict flag
+    celix_properties_t* props2;
+    status = celix_properties_loadFromString2(inputJSON, CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS, &props2);
+
+    //Then loading fails, because the empty array generates an error
+    ASSERT_EQ(CELIX_ILLEGAL_ARGUMENT, status);
+
+    //And at least one error message is added to celix_err
+    ASSERT_GE(celix_err_getErrorCount(), 1);
+}
+
+TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithNestedObjectsTest) {
+    // Given a complex JSON object
+    const char* jsonInput = R"({
+        "key1":"value1",
+        "key2":"value2",
+        "object1": {
+            "key3":"value3",
+            "key4":true
+        },
+        "object2": {
+            "key5":5.0
+        },
+        "object3":{
+            "object4":{
+                "key6":6
+            }
+        }
+    })";
+
+    // And a stream with the JSON object
+    FILE* stream = fmemopen((void*)jsonInput, strlen(jsonInput), "r");
+
+    // When loading the properties from the stream
+    celix_autoptr(celix_properties_t) props = nullptr;
+    auto status = celix_properties_loadFromStream(stream, 0, &props);
+    ASSERT_EQ(CELIX_SUCCESS, status);
+
+    // Then the properties object contains the nested objects
+    EXPECT_EQ(6, celix_properties_size(props));
+    EXPECT_STREQ("value1", celix_properties_getString(props, "key1"));
+    EXPECT_STREQ("value2", celix_properties_getString(props, "key2"));
+    EXPECT_STREQ("value3", celix_properties_getString(props, "object1.key3"));
+    EXPECT_EQ(true, celix_properties_getBool(props, "object1.key4", false));
+    EXPECT_DOUBLE_EQ(5., celix_properties_getDouble(props, "object2.key5", 0.0));
+    EXPECT_EQ(6, celix_properties_getLong(props, "object3.object4.key6", 0));
+}
+
+TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithDuplicatesTest) {
+    // Given a complex JSON object with duplicate keys
+    const char* jsonInput = R"({
+        "key":2,
+        "key":3
+    })";
+
+    // When loading the properties from a string.
+    celix_autoptr(celix_properties_t) props = nullptr;
+    auto status = celix_properties_loadFromString2(jsonInput, 0, &props);
+
+    // Then loading succeeds
+    ASSERT_EQ(CELIX_SUCCESS, status);
+
+    // And the properties object contains the last values of the jpath keys
+    EXPECT_EQ(1, celix_properties_size(props));
+    EXPECT_EQ(3, celix_properties_getLong(props, "key", 0));
+
+    // When decoding the properties from the stream using a flog that does not allow duplicates
+    celix_properties_t* props2;
+    status = celix_properties_loadFromString2(jsonInput, CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES, &props2);
+
+    // Then loading fails, because of a duplicate key
+    EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status);
+
+    // And at least one error message is added to celix_err
+    EXPECT_GE(celix_err_getErrorCount(), 1);
+    celix_err_printErrors(stderr, "Test Error: ", "\n");
+}
+
+TEST_F(PropertiesSerializationTestSuite, LoadPropertiesEscapedDotsTest) {
+    // Given a complex JSON object with collisions and duplicate keys
+    // Collisions:
+    // - object object1/object2 and value object1/object2
+    // - value key1 in object2 in object1 and value object2/key in object1
+    // - value object3/key4 and value key4 in object object3
+    // Duplicate JSON keys:
+    // - key3
+    const char* jsonInput = R"({
+        "object1": {
+            "object2": {
+                "key1": "value1"
+            },
+            "object2.key2": "value2"
+        },
+        "object1.object2" : "value3",
+        "key3": "value4",
+        "key3": "value5",
+        "object3.key4": "value6",
+        "object3": {
+            "key4": "value7"
+        }
+    })";
+
+    // When loading the properties from a string.
+    celix_autoptr(celix_properties_t) props;
+    auto status = celix_properties_loadFromString2(jsonInput, 0, &props);
+
+    // Then loading succeeds
+    ASSERT_EQ(CELIX_SUCCESS, status);
+
+    // And the properties object all the values for the colliding keys and a single (latest) value for the duplicate
+    // keys
+    EXPECT_EQ(5, celix_properties_size(props));
+    EXPECT_STREQ("value1", celix_properties_getString(props, "object1.object2.key1"));
+    EXPECT_STREQ("value2", celix_properties_getString(props, "object1.object2.key2"));
+    EXPECT_STREQ("value3", celix_properties_getString(props, "object1.object2"));
+    EXPECT_STREQ("value5", celix_properties_getString(props, "key3"));
+    EXPECT_STREQ("value7", celix_properties_getString(props, "object3.key4"));
+
+    // When decoding the properties from a string using a flag that allows duplicates
+    celix_properties_t* props2;
+    status = celix_properties_loadFromString2(jsonInput, CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES, &props2);
+
+    // Then loading fails, because of a duplicate key
+    EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status);
+
+    // And at least one error message is added to celix_err
+    EXPECT_GE(celix_err_getErrorCount(), 1);
+    celix_err_printErrors(stderr, "Test Error: ", "\n");
+
+    // When decoding the properties from a string using a flag that allows collisions
+    celix_properties_t* props3;
+    status = celix_properties_loadFromString2(jsonInput, CELIX_PROPERTIES_DECODE_ERROR_ON_COLLISIONS, &props3);
+
+    // Then loading fails, because of a collision
+    EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status);
+
+    // And at least one error message is added to celix_err
+    EXPECT_GE(celix_err_getErrorCount(), 1);
+    celix_err_printErrors(stderr, "Test Error: ", "\n");
+}
+
+TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithAndWithoutStrictFlagTest) {
+    auto invalidInputs = {
+        R"({"mixedArr":["string", true]})", // Mixed array gives error on strict
+        R"({"mixedVersionAndStringArr":["version<1.2.3.qualifier>","2.3.4"]})", // Mixed array gives error on strict
+        R"({"key1":null})",                 // Null value gives error on strict
+        R"({"":"value"})",                  // "" key gives error on strict
+        R"({"emptyArr":[]})",               // Empty array gives error on strict
+        R"({"key1":"val1", "key1":"val2"})"// Duplicate key gives error on strict
+    };
+
+    for (auto& invalidInput: invalidInputs) {
+        //Given an invalid JSON object
+        FILE* stream = fmemopen((void*)invalidInput, strlen(invalidInput), "r");
+
+        //When loading the properties from the stream with an empty flags
+        celix_autoptr(celix_properties_t) props = nullptr;
+        auto status = celix_properties_loadFromStream(stream, 0, &props);
+
+        //Then decoding succeeds, because strict is disabled
+        ASSERT_EQ(CELIX_SUCCESS, status);
+        EXPECT_GE(celix_err_getErrorCount(), 0);
+
+        //But the properties size is 0 or 1, because the all invalid inputs are ignored, except the duplicate key
+        auto size = celix_properties_size(props);
+        EXPECT_TRUE(size == 0 || size == 1);
+
+        fclose(stream);
+    }
+
+    for (auto& invalidInput: invalidInputs) {
+        //Given an invalid JSON object
+        FILE* stream = fmemopen((void*)invalidInput, strlen(invalidInput), "r");
+
+        //When loading the properties from the stream with a strict flag
+        celix_autoptr(celix_properties_t) props = nullptr;
+        auto status = celix_properties_loadFromStream(stream, CELIX_PROPERTIES_DECODE_STRICT, &props);
+
+        //Then decoding fails
+        EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status);
+
+        //And at least one error message is added to celix_err
+        EXPECT_GE(celix_err_getErrorCount(), 1);
+        celix_err_printErrors(stderr, "Test Error: ", "\n");
+
+        fclose(stream);
+    }
+}
+
+TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithUnsupportedArrayTypesTest) {
+    auto invalidArrays = {
+        R"({"objArray":[{"obj1": true}, {"obj2": true}]})", // Array with objects not supported.
+        R"({"arrayArray":[[1,2], [2,4]]})",                  // Array with array not supported.
+        R"({"nullArr":[null,null]})"       // Array with null values gives error on strict
+    };
+
+    // Decode with no strict flag, will ignore the unsupported arrays
+    for (auto& invalidArray : invalidArrays) {
+        // When loading the properties from the string
+        celix_autoptr(celix_properties_t) props = nullptr;
+        auto status = celix_properties_loadFromString2(invalidArray, 0, &props);
+
+        // Then decoding succeeds, because strict is disabled
+        ASSERT_EQ(CELIX_SUCCESS, status);
+        EXPECT_GE(celix_err_getErrorCount(), 0);
+
+        // But the properties size is 0, because the all invalid inputs are ignored
+        EXPECT_EQ(0, celix_properties_size(props));
+    }
+
+    // Decode with strict flag, will fail on unsupported arrays
+    for (auto& invalidArray : invalidArrays) {
+        // When loading the properties from the string
+        celix_autoptr(celix_properties_t) props = nullptr;
+        auto status = celix_properties_loadFromString2(invalidArray, CELIX_PROPERTIES_DECODE_STRICT, &props);
+
+        // Then decoding fails
+        EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status);
+
+        // And at least one error message is added to celix_err
+        EXPECT_GE(celix_err_getErrorCount(), 1);
+        celix_err_resetErrors();
+
+        // When loading the properties from the CELIX_PROPERTIES_DECODE_ERROR_ON_UNSUPPORTED_ARRAYS flag
+        celix_properties_t* props2;
+        status = celix_properties_loadFromString2(
+            invalidArray, CELIX_PROPERTIES_DECODE_ERROR_ON_UNSUPPORTED_ARRAYS, &props2);
+
+        // Then decoding fails
+        EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status);
+
+        // And at least one error message is added to celix_err
+        EXPECT_GE(celix_err_getErrorCount(), 1);
+        //celix_err_resetErrors();
+        celix_err_printErrors(stderr, "Test Error: ", "\n");
+    }
+}
+
+TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithDotsInTheKeysTest) {
+    // Given a complex JSON object
+    const char* jsonInput = R"({
+        ".": "value1",
+        "keyThatEndsWithDots.": "value2",
+        "key..With..Double..Dots": "value3",
+        "object": {
+            ".": "value4",
+            "keyThatEndsWithDot.": "value5",
+            "key..With..Double..Dot": "value6"
+        }
+    })";
+
+    // And a stream with the JSON object
+    FILE* stream = fmemopen((void*)jsonInput, strlen(jsonInput), "r");
+
+    // When loading the properties from the stream
+    celix_autoptr(celix_properties_t) props = nullptr;
+    auto status = celix_properties_loadFromStream(stream, 0, &props);
+    celix_err_printErrors(stderr, "Test Error: ", "\n");
+    ASSERT_EQ(CELIX_SUCCESS, status);
+
+    // Then the properties object contains the nested objects
+    EXPECT_EQ(6, celix_properties_size(props));
+    EXPECT_STREQ("value1", celix_properties_getString(props, "."));
+    EXPECT_STREQ("value2", celix_properties_getString(props, "keyThatEndsWithDots."));
+    EXPECT_STREQ("value3", celix_properties_getString(props, "key..With..Double..Dots"));
+    EXPECT_STREQ("value4", celix_properties_getString(props, "object.."));
+    EXPECT_STREQ("value5", celix_properties_getString(props, "object.keyThatEndsWithDot."));
+    EXPECT_STREQ("value6", celix_properties_getString(props, "object.key..With..Double..Dot"));
+}
+
+TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithInvalidVersionsTest) {
+    // Given a JSON object with an invalid version string (<, > not allowed in qualifier)
+    const auto* jsonInput = R"( {"key":"version<1.2.3.<qualifier>>"} )";
+
+    // When loading the properties from the stream
+    celix_autoptr(celix_properties_t) props = nullptr;
+    auto status = celix_properties_loadFromString2(jsonInput, 0, &props);
+
+    // Then loading fails
+    ASSERT_EQ(CELIX_ILLEGAL_ARGUMENT, status);
+
+    // And at least one error message is added to celix_err
+    EXPECT_GE(celix_err_getErrorCount(), 1);
+    celix_err_printErrors(stderr, "Test Error: ", "\n");
+
+    // Given a JSON object with an invalid version strings, that are not recognized as versions
+    jsonInput =
+        R"( {"key1":"version<1.2.3", "key2":"version1.2.3>", "key3":"ver<1.2.3>}", "key4":"celix_version<1.2.3>"} )";
+
+    // When loading the properties from the stream
+    status = celix_properties_loadFromString2(jsonInput, 0, &props);
+
+    // Then loading succeeds
+    ASSERT_EQ(CELIX_SUCCESS, status);
+
+    // But the values are not recognized as versions
+    EXPECT_NE(CELIX_PROPERTIES_VALUE_TYPE_VERSION, celix_properties_getType(props, "key1"));
+    EXPECT_NE(CELIX_PROPERTIES_VALUE_TYPE_VERSION, celix_properties_getType(props, "key2"));
+    EXPECT_NE(CELIX_PROPERTIES_VALUE_TYPE_VERSION, celix_properties_getType(props, "key3"));
+    EXPECT_NE(CELIX_PROPERTIES_VALUE_TYPE_VERSION, celix_properties_getType(props, "key4"));
+}
+
+TEST_F(PropertiesSerializationTestSuite, LoadWithInvalidStreamTest) {
+    celix_properties_t* dummyProps = nullptr;
+
+    // Loading properties with invalid stream will fail
+    auto status = celix_properties_load2("non_existing_file.json", 0, &dummyProps);
+    EXPECT_EQ(CELIX_FILE_IO_EXCEPTION, status);
+    EXPECT_EQ(1, celix_err_getErrorCount());
+
+    char* buf = nullptr;
+    size_t size = 0;
+    FILE* stream = open_memstream(&buf, &size); // empty stream
+    status = celix_properties_loadFromStream(stream, 0, &dummyProps);
+    EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status);
+    EXPECT_EQ(2, celix_err_getErrorCount());
+
+    fclose(stream);
+    free(buf);
+    celix_err_printErrors(stderr, "Test Error: ", "\n");
+}
+
+TEST_F(PropertiesSerializationTestSuite, LoadCxxPropertiesTest) {
+    // Given a JSON object
+    auto jsonInput = R"({"key1":"value1","key2":42,"key2":43})"; // note duplicate key3
+
+    // When loading the properties from the JSON object
+    auto props = celix::Properties::loadFromString(jsonInput);
+
+    // Then the properties object contains the values
+    EXPECT_EQ(2, props.size());
+    EXPECT_STREQ("value1", props.get("key1").c_str());
+    EXPECT_EQ(43, props.getAsLong("key2", -1));
+
+    // When loading the properties from the JSON object with a strict flag
+    EXPECT_THROW(celix::Properties::loadFromString(jsonInput, celix::Properties::DecodeFlags::Strict),
+                 celix::IllegalArgumentException);
+
+    // When loading the properties from the JSON object with a flag combined
+    EXPECT_THROW(
+        celix::Properties::loadFromString(
+            jsonInput,
+            celix::Properties::DecodeFlags::ErrorOnCollisions | celix::Properties::DecodeFlags::ErrorOnDuplicates |
+                celix::Properties::DecodeFlags::ErrorOnEmptyArrays | celix::Properties::DecodeFlags::ErrorOnEmptyKeys |
+                celix::Properties::DecodeFlags::ErrorOnUnsupportedArrays |
+                celix::Properties::DecodeFlags::ErrorOnNullValues | celix::Properties::DecodeFlags::ErrorOnNullValues),
+        celix::IllegalArgumentException);
+
+    EXPECT_THROW(celix::Properties::load2("non_existing_file.json"), celix::IOException);
+}
+
+TEST_F(PropertiesSerializationTestSuite, SaveAndLoadFlatProperties) {
+    // Given a properties object with all possible types (but no empty arrays)
+    celix_autoptr(celix_properties_t) props = celix_properties_create();
+
+    celix_properties_set(props, "single/strKey", "strValue");
+    celix_properties_setLong(props, "single/longKey", 42);
+    celix_properties_setDouble(props, "single/doubleKey", 2.0);
+    celix_properties_setBool(props, "single/boolKey", true);
+    celix_properties_assignVersion(props, "single/versionKey", celix_version_create(1, 2, 3, "qualifier"));
+
+    celix_array_list_t* strArr = celix_arrayList_createStringArray();
+    celix_arrayList_addString(strArr, "value1");
+    celix_arrayList_addString(strArr, "value2");
+    celix_properties_assignArrayList(props, "array/stringArr", strArr);
+
+    celix_array_list_t* longArr = celix_arrayList_createLongArray();
+    celix_arrayList_addLong(longArr, 1);
+    celix_arrayList_addLong(longArr, 2);
+    celix_properties_assignArrayList(props, "array/longArr", longArr);
+
+    celix_array_list_t* doubleArr = celix_arrayList_createDoubleArray();
+    celix_arrayList_addDouble(doubleArr, 1.0);
+    celix_arrayList_addDouble(doubleArr, 2.0);
+    celix_properties_assignArrayList(props, "array/doubleArr", doubleArr);
+
+    celix_array_list_t* boolArr = celix_arrayList_createBoolArray();
+    celix_arrayList_addBool(boolArr, true);
+    celix_arrayList_addBool(boolArr, false);
+    celix_properties_assignArrayList(props, "array/boolArr", boolArr);
+
+    celix_array_list_t* versionArr = celix_arrayList_createVersionArray();
+    celix_arrayList_assignVersion(versionArr, celix_version_create(1, 2, 3, "qualifier"));
+    celix_arrayList_assignVersion(versionArr, celix_version_create(4, 5, 6, "qualifier"));
+    celix_properties_assignArrayList(props, "array/versionArr", versionArr);
+
+    // When saving the properties to a properties_test.json file
+    const char* filename = "properties_test.json";
+    auto status = celix_properties_save(props, filename, CELIX_PROPERTIES_ENCODE_PRETTY);
+
+    // Then saving succeeds
+    ASSERT_EQ(CELIX_SUCCESS, status);
+
+    // When loading the properties from the properties_test.json file
+    celix_autoptr(celix_properties_t) loadedProps = nullptr;
+    status = celix_properties_load2(filename, 0, &loadedProps);
+
+    // Then loading succeeds
+    ASSERT_EQ(CELIX_SUCCESS, status);
+
+    // And the loaded properties are equal to the original properties
+    EXPECT_TRUE(celix_properties_equals(props, loadedProps));
+}
+
+TEST_F(PropertiesSerializationTestSuite, SaveAndLoadCxxProperties) {
+    //Given a filename
+    std::string filename = "properties_test.json";
+
+    //And a Properties object with 1 key
+    celix::Properties props{};
+    props.set("key1", "value1");
+
+    //When saving the properties to the filename
+    props.save(filename);
+
+    //And reloading the properties from the filename
+    auto props2 = celix::Properties::load2(filename);
+
+    //Then the reloaded properties are equal to the original properties
+    EXPECT_TRUE(props == props2);
+}
+
+TEST_F(PropertiesSerializationTestSuite, JsonErrorToCelixStatusTest) {
+    EXPECT_EQ(CELIX_ILLEGAL_STATE, celix_properties_jsonErrorToStatus(json_error_unknown));
+
+    EXPECT_EQ(ENOMEM, celix_properties_jsonErrorToStatus(json_error_out_of_memory));
+    EXPECT_EQ(ENOMEM, celix_properties_jsonErrorToStatus(json_error_stack_overflow));
+
+    EXPECT_EQ(CELIX_FILE_IO_EXCEPTION, celix_properties_jsonErrorToStatus(json_error_cannot_open_file));
+
+    EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_invalid_argument));
+    EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_invalid_argument));
+    EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_premature_end_of_input));
+    EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_end_of_input_expected));
+    EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_invalid_syntax));
+    EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_invalid_format));
+    EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_wrong_type));
+    EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_null_character));
+    EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_null_value));
+    EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_null_byte_in_key));
+    EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_duplicate_key));
+    EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_numeric_overflow));
+    EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_item_not_found));
+    EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_index_out_of_range));
+}
+
+TEST_F(PropertiesSerializationTestSuite, KeyCollision) {
+    celix_autoptr(celix_properties_t) props = celix_properties_create();
+    // pick keys such that key1 appears before key2 when iterating over the properties
+    celix_properties_set(props, "a.b.haha.arbifdadfsfa", "value1");
+    celix_properties_set(props, "a.b.haha", "value2");
+
+    celix_autofree char* output = nullptr;
+    auto status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED_STYLE | CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS,
+                                                    &output);
+    EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status);
+
+    celix_autoptr(celix_properties_t) props2 = celix_properties_create();
+    // pick keys such that key1 appears before key2 when iterating over the properties
+    celix_properties_set(props2, "a.b.c", "value1");
+    celix_properties_set(props2, "a.b.c.d", "value2");
+    status = celix_properties_saveToString(props2, CELIX_PROPERTIES_ENCODE_NESTED_STYLE | CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS,
+                                           &output);
+    EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status);
+    status = celix_properties_saveToString(props2, CELIX_PROPERTIES_ENCODE_NESTED_STYLE, &output);
+    // "a.b.c.d" is silently discarded
+    EXPECT_STREQ(R"({"a":{"b":{"c":"value1"}}})", output);
+    std::cout << output << std::endl;
+    EXPECT_EQ(CELIX_SUCCESS, status);
+}
diff --git a/libs/utils/gtest/src/PropertiesErrorInjectionTestSuite.cc b/libs/utils/gtest/src/PropertiesErrorInjectionTestSuite.cc
index 162a86f..e7916ea 100644
--- a/libs/utils/gtest/src/PropertiesErrorInjectionTestSuite.cc
+++ b/libs/utils/gtest/src/PropertiesErrorInjectionTestSuite.cc
@@ -46,6 +46,7 @@
         celix_ei_expect_open_memstream(nullptr, 0, nullptr);
         celix_ei_expect_asprintf(nullptr, 0, -1);
         celix_ei_expect_malloc(nullptr, 0, nullptr);
+        celix_ei_expect_calloc(nullptr, 0, nullptr);
         celix_ei_expect_celix_stringHashMap_createWithOptions(nullptr, 0, nullptr);
         celix_ei_expect_celix_arrayList_copy(nullptr, 0, nullptr);
         celix_ei_expect_celix_utils_strdup(nullptr, 0, nullptr);
@@ -453,11 +454,12 @@
     celix_err_resetErrors();
 
     celix_autoptr(celix_version_t) version2 = celix_version_create(1, 2, 3, "aaaaaaaaaaaaaaaaaaaaaaaaaa");
-    celix_ei_expect_asprintf((void*) celix_version_toString, 0, -1);
+    celix_ei_expect_calloc((void*) celix_version_create, 0, nullptr);
     status = celix_properties_setVersion(props, "key", version2);
     ASSERT_EQ(status, CELIX_ENOMEM);
-    ASSERT_STREQ("Cannot fill property entry", celix_err_popLastError());
-    ASSERT_STREQ("Failed to allocate memory for celix_version_toString", celix_err_popLastError());
+    EXPECT_EQ(2, celix_err_getErrorCount());
+    ASSERT_STREQ("Failed to copy version", celix_err_popLastError());
+    ASSERT_STREQ("Failed to allocate memory for celix_version_create", celix_err_popLastError());
     celix_err_resetErrors();
 
     fillOptimizationCache(props);
diff --git a/libs/utils/gtest/src/PropertiesTestSuite.cc b/libs/utils/gtest/src/PropertiesTestSuite.cc
index 90636b7..f44e0b0 100644
--- a/libs/utils/gtest/src/PropertiesTestSuite.cc
+++ b/libs/utils/gtest/src/PropertiesTestSuite.cc
@@ -24,6 +24,7 @@
 #include "celix_err.h"
 #include "celix_properties.h"
 #include "celix_properties_internal.h"
+#include "celix_stdlib_cleanup.h"
 #include "celix_utils.h"
 
 using ::testing::MatchesRegex;
@@ -1005,3 +1006,10 @@
     //And when an NULL key is used, a ILLEGAL_ARGUMENT error is returned
     EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_setArrayList(props, nullptr, list));
 }
+
+TEST_F(PropertiesTestSuite, EmptyStringKeyTest) {
+    celix_autoptr(celix_properties_t) props = celix_properties_create();
+    celix_properties_set(props, "", "value"); // "" is a valid key (nullptr is not)
+    EXPECT_EQ(1, celix_properties_size(props));
+    EXPECT_STREQ("value", celix_properties_getString(props, ""));
+}
diff --git a/libs/utils/include/celix/Exception.h b/libs/utils/include/celix/Exception.h
deleted file mode 100644
index 481d6f8..0000000
--- a/libs/utils/include/celix/Exception.h
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-#pragma once
-
-#include <stdexcept>
-
-namespace celix {
-
-    /**
-     * @brief Celix runtime Exception
-     */
-    class Exception : public std::runtime_error {
-    public:
-        using std::runtime_error::runtime_error;
-    };
-
-}
diff --git a/libs/utils/include/celix/Exceptions.h b/libs/utils/include/celix/Exceptions.h
new file mode 100644
index 0000000..d6d24a1
--- /dev/null
+++ b/libs/utils/include/celix/Exceptions.h
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+#pragma once
+
+#include "celix_errno.h"
+
+#include <stdexcept>
+#include <cassert>
+
+namespace celix {
+
+    /**
+     * @brief Celix Generic Exception
+     */
+    class Exception : public std::runtime_error {
+    public:
+        using std::runtime_error::runtime_error;
+    };
+
+    /**
+     * @brief Celix Illegal Argument Exception
+     */
+    class IllegalArgumentException final: public Exception {
+    public:
+        using Exception::Exception;
+    };
+
+    /**
+     * @brief Celix IO Exception
+     */
+    class IOException final: public Exception {
+    public:
+        using Exception::Exception;
+    };
+
+    namespace impl {
+        /**
+        * @brief Utils function to throw a celix::Exception using the given celix_status_t and message.
+        */
+        void inline throwException(celix_status_t status, const std::string& message) {
+            assert(status != CELIX_SUCCESS);
+            const auto* statusMsg = celix_strerror(status);
+            auto msg = std::string{message} + " (" + statusMsg + ")";
+            switch (status) {
+                case CELIX_ILLEGAL_ARGUMENT:
+                    throw celix::IllegalArgumentException{msg};
+                case CELIX_FILE_IO_EXCEPTION:
+                    throw celix::IOException{msg};
+                default:
+                    throw celix::Exception{msg};
+            }
+        }
+    }
+
+}
diff --git a/libs/utils/include/celix/Filter.h b/libs/utils/include/celix/Filter.h
index aecda6d..7a565a9 100644
--- a/libs/utils/include/celix/Filter.h
+++ b/libs/utils/include/celix/Filter.h
@@ -25,7 +25,7 @@
 #include "celix_filter.h"
 #include "celix_err.h"
 
-#include "celix/Exception.h"
+#include "celix/Exceptions.h"
 #include "celix/Properties.h"
 
 namespace celix {
diff --git a/libs/utils/include/celix/IOException.h b/libs/utils/include/celix/IOException.h
deleted file mode 100644
index 1715801..0000000
--- a/libs/utils/include/celix/IOException.h
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-#pragma once
-
-#include <exception>
-#if __cplusplus >= 201703L //C++17 or higher
-#include <string_view>
-#endif
-
-namespace celix {
-
-    /**
-     * @brief Celix runtime IO Exception
-     */
-    class IOException : public std::exception {
-    public:
-#if __cplusplus >= 201703L //C++17 or higher
-        explicit IOException(std::string_view msg) : w{msg} {}
-#else
-        explicit IOException(std::string msg) : w{std::move(msg)} {}
-#endif
-
-        IOException(const IOException&) = default;
-        IOException(IOException&&) = default;
-        IOException& operator=(const IOException&) = default;
-        IOException& operator=(IOException&&) = default;
-
-        [[nodiscard]] const char* what() const noexcept override {
-            return w.c_str();
-        }
-    private:
-        std::string w;
-    };
-
-}
diff --git a/libs/utils/include/celix/Properties.h b/libs/utils/include/celix/Properties.h
index 3b784c5..ee71925 100644
--- a/libs/utils/include/celix/Properties.h
+++ b/libs/utils/include/celix/Properties.h
@@ -30,7 +30,7 @@
 #include "celix_properties.h"
 #include "celix_utils.h"
 #include "celix/Version.h"
-#include "celix/IOException.h"
+#include "celix/Exceptions.h"
 
 namespace celix {
 
@@ -948,14 +948,168 @@
         }
 
         /**
+         * @brief Enum class for encoding flags used in Celix properties JSON encoding.
+         *
+         * The flags are used to control the encoding process and to specify the output format.
+         *
+         * @enum EncodingFlags
+         */
+        enum class EncodingFlags : int {
+            None = 0, /**< No special encoding flags. */
+            Pretty =
+                CELIX_PROPERTIES_ENCODE_PRETTY, /**< Encode in a pretty format, with indentation and line breaks. */
+            FlatStyle =
+                CELIX_PROPERTIES_ENCODE_FLAT_STYLE, /**< Encode in a flat style, with all keys at the top level. */
+            NestedStyle = CELIX_PROPERTIES_ENCODE_NESTED_STYLE, /**< Encode in a nested style, with nested objects for
+                                                                   each key based on a `/` separator. */
+            ErrorOnCollisions = CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS,    /**< If set, encoding will fail if there
+                                                                                   are collisions between keys. */
+            ErrorOnEmptyArrays = CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS, /**< If set, encoding will fail if there
+                                                                                   are empty arrays. */
+            ErrorOnNanInf = CELIX_PROPERTIES_ENCODE_ERROR_ON_NAN_INF, /**< If set, encoding will fail if there are NaN
+                                                                         or Inf values. */
+            Strict = CELIX_PROPERTIES_ENCODE_STRICT, /**< If set, encoding will fail if there are any errors. */
+        };
+
+        /**
+         * @brief Save (encode) this properties object as a JSON representation to a file.
+         *
+         * For more information how a properties object is encoded to JSON, see the celix_properties_loadFromStream
+         *
+         * For a overview of the possible encode flags, see the EncodingFlags flags documentation.
+         * The default encoding style is a compact and flat JSON representation.
+         *
+         * @param[in] filename The file to write the JSON representation of the properties object to.
+         * @param[in] encodingFlags The flags to use when encoding the input string.
+         * @throws celix::IOException If an error occurs while writing to the file.
+         * @throws celix::IllegalArgumentException If the provided properties cannot be encoded to JSON.
+         * @throws std::bad_alloc If there was not enough memory to save the properties.
+         */
+        void save(const std::string& filename, EncodingFlags encodingFlags = EncodingFlags::None) const {
+            auto status = celix_properties_save(cProps.get(), filename.c_str(), static_cast<int>(encodingFlags));
+            if (status == ENOMEM) {
+                throw std::bad_alloc();
+            } else if (status != CELIX_SUCCESS) {
+                celix::impl::throwException(status, std::string{"Cannot save celix::Properties to "} + filename);
+            }
+        }
+
+        /**
+         * @brief Save (encode) this properties object as a JSON representation to a string.
+         *
+         * For more information how a properties object is encoded to JSON, see the celix_properties_loadFromStream
+         *
+         * For a overview of the possible encode flags, see the EncodingFlags flags documentation.
+         * The default encoding style is a compact and flat JSON representation.
+         *
+         * @param[in] encodeFlags The flags to use when encoding the input string.
+         * @throws celix::IllegalArgumentException If the provided properties cannot be encoded to JSON.
+         * @throws std::bad_alloc If there was not enough memory to save the properties.
+         */
+        std::string saveToString(EncodingFlags encodeFlags = EncodingFlags::None) const {
+            char* str = nullptr;
+            auto status = celix_properties_saveToString(cProps.get(), static_cast<int>(encodeFlags), &str);
+            if (status == ENOMEM) {
+                throw std::bad_alloc();
+            } else if (status != CELIX_SUCCESS) {
+                celix::impl::throwException(status, "Cannot save celix::Properties to string");
+            }
+            std::string result{str};
+            free(str);
+            return result;
+        }
+
+        /**
+         * @brief Enum class for decoding flags used in Celix properties JSON decoding.
+         *
+         * The flags are used to control the decoding process and to specify the output format.
+         *
+         * @enum DecodeFlags
+         */
+        enum class DecodeFlags : int {
+            None = 0,                                                           /**< No special decoding flags. */
+            ErrorOnDuplicates = CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES,    /**< If set, decoding will fail if there
+                                                                                   are duplicate keys. */
+            ErrorOnCollisions = CELIX_PROPERTIES_DECODE_ERROR_ON_COLLISIONS,    /**< If set, decoding will fail if there
+                                                                                   are collisions between keys. */
+            ErrorOnNullValues = CELIX_PROPERTIES_DECODE_ERROR_ON_NULL_VALUES,   /**< If set, decoding will fail if there
+                                                                                   are null values. */
+            ErrorOnEmptyArrays = CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS, /**< If set, decoding will fail if there
+                                                                                   are empty arrays. */
+            ErrorOnEmptyKeys = CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS, /**< If set, decoding will fail if there are
+                                                                               empty ("") keys. */
+            ErrorOnUnsupportedArrays =
+                CELIX_PROPERTIES_DECODE_ERROR_ON_UNSUPPORTED_ARRAYS, /**< If set, decoding will fail if there
+                                                            are unsupported array types or mixed array types. */
+            Strict = CELIX_PROPERTIES_DECODE_STRICT /**< If set, decoding will fail if there are any errors. */
+        };
+
+        /**
          * @brief Loads properties from the file at the given path.
          * @param[in] path The path to the file containing the properties.
          * @return A new Properties object containing the properties from the file.
          * @throws celix::IOException If the file cannot be opened or read.
+         * @throws celix::IllegalArgumentException if the provided input cannot be decoded to a properties object.
+         * @throws std::bad_alloc If there was not enough memory to load the properties.
          */
-        static celix::Properties load(const std::string& path) { return loadFrom(path.data()); }
+        static Properties load(const std::string& path) { return loadFrom(path.data()); }
 
-    private:
+        /**
+         * @brief Load a Properties object from a file.
+         *
+         * @warning The name is temporary and will be renamed to celix::Properties::load in the future (when
+         * the current celix::Properties::load is removed).
+         *
+         * The content of the filename file is expected to be in the format of a JSON object.
+         * For what can and cannot be parsed, see celix_properties_loadFromStream documentation.
+         *
+         * For a overview of the possible decode flags, see the DecodingFlags flags documentation.
+         *
+         * @param[in] filename The file to load the properties from.
+         * @param[in] decodeFlags The flags to use when decoding the input string.
+         * @return A new Properties object containing the properties from the file.
+         * @throws celix::IOException If the file cannot be opened or read.
+         * @throws celix::IllegalArgumentException if the provided input cannot be decoded to a properties object.
+         * @throws std::bad_alloc If there was not enough memory to load the properties.
+         */
+        static Properties load2(const std::string& filename, DecodeFlags decodeFlags = DecodeFlags::None) {
+            celix_properties_t* props;
+            auto status = celix_properties_load2(filename.c_str(), static_cast<int>(decodeFlags), &props);
+            if (status == ENOMEM) {
+                throw std::bad_alloc();
+            } else if (status != CELIX_SUCCESS) {
+                celix::impl::throwException(status, "Cannot load celix::Properties from " + filename);
+            }
+            return celix::Properties::own(props);
+        }
+
+        /**
+         * @brief Load a Properties object from a string.
+         *
+         *
+         * The input string is expected to be in the format of a JSON object.
+         * For what can and cannot be parsed, see celix_properties_loadFromStream documentation.
+         *
+         * For a overview of the possible decode flags, see the DecodingFlags flags documentation.
+         *
+         * @param[in] input The input string to parse.
+         * @param[in] decodeFlags The flags to use when decoding the input string.
+         * @return A new Properties object containing the properties from the file.
+         * @throws celix::IllegalArgumentException if the provided input cannot be decoded to a properties object.
+         * @throws std::bad_alloc If there was not enough memory to load the properties.
+         */
+        static Properties loadFromString(const std::string& input, DecodeFlags decodeFlags = DecodeFlags::None) {
+            celix_properties_t* props;
+            auto status = celix_properties_loadFromString2(input.c_str(), static_cast<int>(decodeFlags), &props);
+            if (status == ENOMEM) {
+                throw std::bad_alloc();
+            } else if (status != CELIX_SUCCESS) {
+                celix::impl::throwException(status, "Cannot load celix::Properties from string");
+            }
+            return celix::Properties::own(props);
+        }
+
+      private:
         Properties(celix_properties_t* props, bool takeOwnership) :
             cProps{props, [takeOwnership](celix_properties_t* p){ if (takeOwnership) { celix_properties_destroy(p); }}} {}
 
@@ -1045,8 +1199,36 @@
     };
 }
 
+/**
+ * @brief Stream operator to print the properties value reference to a stream.
+ * @param[in] os The stream to print the properties to.
+ * @param[in] ref The properties value reference to print.
+ * @return The os stream.
+ */
 inline std::ostream& operator<<(std::ostream& os, const ::celix::Properties::ValueRef& ref)
 {
     os << std::string{ref.getValue()};
     return os;
 }
+
+/**
+ * @brief Bitwise OR operator for EncodingFlags.
+ * @param[in] a encoding flags
+ * @param[in] b encoding flags
+ * @return The bitwise OR of the two encoding flags.
+ */
+inline ::celix::Properties::EncodingFlags operator|(::celix::Properties::EncodingFlags a,
+                                                    ::celix::Properties::EncodingFlags b) {
+    return static_cast<::celix::Properties::EncodingFlags>(static_cast<int>(a) | static_cast<int>(b));
+}
+
+/**
+ * @brief Bitwise OR operator for DecodeFlags.
+ * @param[in] a decoding flags
+ * @param[in] b decoding flags
+ * @return The bitwise OR of the two decoding flags.
+ */
+inline ::celix::Properties::DecodeFlags operator|(::celix::Properties::DecodeFlags a,
+                                                  ::celix::Properties::DecodeFlags b) {
+    return static_cast<::celix::Properties::DecodeFlags>(static_cast<int>(a) | static_cast<int>(b));
+}
diff --git a/libs/utils/include/celix_properties.h b/libs/utils/include/celix_properties.h
index 7c7a3e9..f49d462 100644
--- a/libs/utils/include/celix_properties.h
+++ b/libs/utils/include/celix_properties.h
@@ -57,7 +57,7 @@
  */
 typedef enum celix_properties_value_type {
     CELIX_PROPERTIES_VALUE_TYPE_UNSET = 0,   /**< Property value is not set. */
-    CELIX_PROPERTIES_VALUE_TYPE_STRING = 1,  /**< Property value is a string. */
+    CELIX_PROPERTIES_VALUE_TYPE_STRING = 1,  /**< Property value is a UTF-8 encoded string. */
     CELIX_PROPERTIES_VALUE_TYPE_LONG = 2,    /**< Property value is a long integer. */
     CELIX_PROPERTIES_VALUE_TYPE_DOUBLE = 3,  /**< Property value is a double. */
     CELIX_PROPERTIES_VALUE_TYPE_BOOL = 4,    /**< Property value is a boolean. */
@@ -936,6 +936,357 @@
          !celix_propertiesIterator_isEnd(&(iterName));                                                                 \
          celix_propertiesIterator_next(&(iterName)))
 
+/**
+ * @brief Flag to indicate that the encoded output should be pretty; e.g. encoded with additional whitespaces,
+ * newlines and indentation.
+ *
+ * If this flag is not set, the encoded output will compact; e.g. without additional whitespaces, newlines and
+ * indentation.
+ */
+#define CELIX_PROPERTIES_ENCODE_PRETTY 0x01
+
+/**
+ * @brief Flag to indicate that the encoded output should be flat; e.g. all properties entries are written as top level
+ * field entries.
+ *
+ * E.g:
+ * @code{.c}
+ * celix_properties_t* properties = celix_properties_create();
+ * celix_properties_setString(properties, "key/with/slash", "value1");
+ * celix_properties_setString(properties, "key", "value2");
+ * char* json;
+ * celix_properties_saveToString(properties, CELIX_PROPERTIES_ENCODE_FLAT, &json);
+ * // json will be: {"key/with/slash": "value1", "key": "value2"}
+ * @endcode
+ *
+ * Note that encoding with a flat encoding style, all properties keys are unique JSON keys and can be written.
+ *
+ * If no encoding style flag is set, the encoded output will use the default encoding style.
+ */
+#define CELIX_PROPERTIES_ENCODE_FLAT_STYLE 0x02
+
+/**
+ * @brief Flag to indicate that the encoded output should be nested; e.g. properties entries are split on '/' and nested
+ * in JSON objects.
+ *
+ * E.g:
+ * @code{.c}
+ * celix_properties_t* properties = celix_properties_create();
+ * celix_properties_setString(properties, "key/with/slash", "value1");
+ * celix_properties_setString(properties, "key", "value2");
+ * char* json;
+ * celix_properties_saveToString(properties, CELIX_PROPERTIES_ENCODE_NESTED, &json);
+ * // json will be: {"key":{"with":{"slash": "value1"}}}
+ * // or
+ * // json will be: {"key": "value2"}
+ * @endcode
+ *
+ * Note that encoding with a nested encoding style, it properties key can collide resulting in missing properties
+ * entries or (if CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS is set) an error.
+ *
+ * If no encoding style flag is set, the encoded output will use the default encoding style.
+ */
+#define CELIX_PROPERTIES_ENCODE_NESTED_STYLE 0x04
+
+/**
+ * @brief Flag to indicate that the encoding should fail if the JSON representation will contain colliding keys.
+ *
+ * Note that colliding keys can only occur when using the nested encoding style.
+ *
+ * E.g. the following will lead to an error:
+ * @code{.c}
+ * celix_properties_t* properties = celix_properties_create();
+ * celix_properties_setString(properties, "key/with/slash", "value1");
+ * celix_properties_setString(properties, "key", "value2"); //collision
+ * char* json;
+ * celix_status_t status = celix_properties_saveToString(properties,
+ *     CELIX_PROPERTIES_ENCODE_NESTED | | CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS, &json);
+ * // status will be CELIX_ILLEGAL_ARGUMENT and a error message will be logged to celix_err
+ * @endcode
+ *
+ * If this flag is set, the encoding will fail if the JSON representation will contain colliding keys and if this flag
+ * is not set, the encoding will not fail and the colliding keys will be ignored.
+ */
+#define CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS 0x10
+
+/**
+ * @brief Flag to indicate that the encoding should fail if the JSON representation will contain empty arrays.
+ *
+ * Although empty arrays are valid in JSON, they cannot be decoded to a valid properties array entry and as such
+ * empty arrays properties entries are not encoded.
+ *
+ * If this flag is set, the encoding will fail if the JSON representation will contain empty arrays and if this flag
+ * is not set, the encoding will not fail and the empty arrays will be ignored.
+ */
+#define CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS 0x20
+
+/**
+ * @brief Flag to indicate that the encoding should fail if the JSON representation will contain NaN or Inf values.
+ *
+ * NaN, Inf and -Inf are not valid JSON values and as such properties entries with these values are not encoded.
+ *
+ * If this flag is set, the encoding will fail if the JSON representation will contain NaN or Inf values and if this
+ * flag is not set, the encoding will not fail and the NaN and Inf entries will be ignored.
+ */
+#define CELIX_PROPERTIES_ENCODE_ERROR_ON_NAN_INF 0x40
+
+/**
+ * @brief Flag to indicate that all encode "error on" flags should be set.
+ */
+#define CELIX_PROPERTIES_ENCODE_STRICT                                                                                 \
+    (CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS | CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS |                     \
+     CELIX_PROPERTIES_ENCODE_ERROR_ON_NAN_INF)
+
+/**
+ * @brief Save (encode) as a JSON representation to a stream.
+ *
+ * The stream is expected to be a valid stream and is not reset or closed by this function.
+ *
+ * Properties are encoded as a JSON object.
+ *
+ * If no encoding style flag is set or when the CELIX_PROPERTIES_ENCODE_FLAT_STYLE flag is set, properties
+ * entries are written as top level field entries.
+ *
+ * If the CELIX_PROPERTIES_ENCODE_NESTED_STYLE flag is set, properties entry keys are split on '/' and nested in
+ * JSON objects. This leads to a more natural JSON representation, but if there are colliding properties keys (e.g.
+ * `{"key": "value1", "key/with/slash": "value2"}`), not all properties entries will be written.
+ *
+ * With all encoding styles, the empty array properties entries are ignored, because they cannot be decoded to a valid
+ * properties array entry.
+ *
+ * Properties type entries are encoded as follows:
+ * - CELIX_PROPERTIES_TYPE_STRING: The value is encoded as a JSON string.
+ * - CELIX_PROPERTIES_TYPE_LONG: The value is encoded as a JSON number.
+ * - CELIX_PROPERTIES_TYPE_DOUBLE: The value is encoded as a JSON number.
+ * - CELIX_PROPERTIES_TYPE_BOOL: The value is encoded as a JSON boolean.
+ * - CELIX_PROPERTIES_TYPE_ARRAY: The value is encoded as a JSON array, with each element encoded according to its type.
+ * - CELIX_PROPERTIES_TYPE_VERSION: The value is encoded as a JSON string with a "version<" prefix and a ">" suffix
+ * (e.g. "version<1.2.3>").
+ *
+ * For a overview of the possible encode flags, see the CELIX_PROPERTIES_ENCODE_* flags documentation.
+ * The default encoding style is a compact and flat JSON representation.
+ *
+ * @param properties The properties object to encode.
+ * @param stream The stream to write the JSON representation of the properties object to.
+ * @param encodeFlags The flags to use when encoding the input properties.
+ * @return CELIX_SUCCESS if the operation was successful, CELIX_ILLEGAL_ARGUMENT if the provided properties cannot be
+ * encoded to a JSON representation, ENOMEM if there was not enough memory and CELIX_FILE_IO_EXCEPTION if the stream
+ * could not be written to.
+ */
+CELIX_UTILS_EXPORT celix_status_t celix_properties_saveToStream(const celix_properties_t* properties,
+                                                                FILE* stream,
+                                                                int encodeFlags);
+
+/**
+ * @brief Save (encode) properties as a JSON representation to a file.
+ *
+ * For more information how a properties object is encoded to JSON, see the celix_properties_loadFromStream
+ *
+ * For a overview of the possible encode flags, see the CELIX_PROPERTIES_ENCODE_* flags documentation.
+ * The default encoding style is a compact and flat JSON representation.
+ *
+ * @param[in] properties The properties object to encode.
+ * @param[in] filename The file to write the JSON representation of the properties object to.
+ * @param[in] encodeFlags The flags to use when encoding the input properties.
+ * @return CELIX_SUCCESS if the operation was successful, CELIX_ILLEGAL_ARGUMENT if the provided properties cannot be
+ * encoded to a JSON representation and ENOMEM if there was not enough memory. CELIX_FILE_IO_EXCEPTION if the file
+ * could not be opened or written to.
+ */
+CELIX_UTILS_EXPORT celix_status_t celix_properties_save(const celix_properties_t* properties,
+                                                        const char* filename,
+                                                        int encodeFlags);
+
+/**
+ * @brief Save (encode) properties as a JSON representation to a string.
+ *
+ * For more information how a properties object is encoded to JSON, see the celix_properties_loadFromStream
+ *
+ * For a overview of the possible encode flags, see the CELIX_PROPERTIES_ENCODE_* flags documentation.
+ * The default encoding style is a compact and flat JSON representation.
+ *
+ * @param[in] properties The properties object to encode.
+ * @param[in] encodeFlags The flags to use when encoding the input properties.
+ * @param[out] out The JSON string representation of the properties object. The caller is responsible for freeing the
+ * returned string using free.
+ * @return CELIX_SUCCESS if the operation was successful, CELIX_ILLEGAL_ARGUMENT if the provided properties cannot be
+ * encoded to a JSON representation and ENOMEM if there was not enough memory.
+ */
+CELIX_UTILS_EXPORT celix_status_t celix_properties_saveToString(const celix_properties_t* properties,
+                                                                int encodeFlags,
+                                                                char** out);
+
+/**
+ * @brief Flag to indicate that the decoding should fail if the input contains duplicate JSON keys.
+ *
+ * E.g. `{"key": "value", "key": "value2"}` is a duplicate key.
+ *
+ * If this flag is set, the decoding will fail if the input contains a duplicate key and if this flag is not set, the
+ * decoding will not fail and the last entry will be used.
+ */
+#define CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES 0x01
+
+/**
+ * @brief Flag to indicate that the decoding should fail if the input contains entry that collide on property keys.
+ *
+ * E.g. `{"obj/key": "value", "obj": {"key": "value2"}}` is a collision.
+ *
+ * If this flag is set, the decoding will fail if the input contains a collision and if this flag is not set, the
+ * decoding will not fail and the last entry will be used.
+ */
+#define CELIX_PROPERTIES_DECODE_ERROR_ON_COLLISIONS 0x02
+
+/**
+ * @brief Flag to indicate that the decoding should fail if the input contains null values.
+ *
+ * E.g. `{"key": null}` is a null value.
+ *
+ * Note arrays with null values are handled by the CELIX_PROPERTIES_DECODE_ERROR_ON_UNSUPPORTED_ARRAYS flag.
+ *
+ * If this flag is set, the decoding will fail if the input contains a null value and if this flag is not set, the
+ * decoding will not fail and the JSON null entry will be ignored.
+ */
+#define CELIX_PROPERTIES_DECODE_ERROR_ON_NULL_VALUES 0x04
+
+/**
+ * @brief Flag to indicate that the decoding should fail if the input contains empty arrays.
+ *
+ *
+ * E.g. `{"key": []}` is an empty array.
+ *
+ * Note that empty arrays are valid in JSON, but not cannot be decoded to a valid properties array entry.
+ *
+ * If this flag is set, the decoding will fail if the input contains an empty array and if this flag is not set, the
+ * decoding will not fail and the JSON empty array entry will be ignored.
+ */
+#define CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS 0x08
+
+/**
+ * @brief Flag to indicate that the decoding should fail if the input contains unsupported arrays.
+ *
+ * Unsupported arrays are arrays that contain JSON objects, multiple arrays, arrays with null values and
+ * mixed arrays.
+ * E.g.
+ * - `{"key": [{"nested": "value"}]}` (array with JSON object)
+ * - `{"key": [[1,2],[3,4]]}` (array with array)
+ * - `{"key": [null,null]}` (array with null values)
+ * - `{"key": ["value", 1]}` (mixed array)
+ *
+ * If this flag is set, the decoding will fail if the input contains an unsupported array and if this flag is not set,
+ * the decoding will not fail and the unsupported array entries will be ignored.
+ */
+#define CELIX_PROPERTIES_DECODE_ERROR_ON_UNSUPPORTED_ARRAYS 0x10
+
+/**
+ * @brief Flag to indicate that the decoding should fail if the input contains empty keys.
+ *
+ * E.g. `{"": "value"}` is an empty key.
+ *
+ * Note that empty keys are valid in JSON and valid in properties, but not always desired.
+ *
+ * If this flag is set, the decoding will fail if the input contains an empty key.
+ */
+#define CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS 0x20
+
+/**
+ * @brief Flag to indicate that the decoding should fail if the input contains any of the decode error flags.
+ *
+ * This flag is a combination of all decode error flags.
+ */
+#define CELIX_PROPERTIES_DECODE_STRICT                                                                                 \
+    (CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES | CELIX_PROPERTIES_DECODE_ERROR_ON_COLLISIONS |                       \
+     CELIX_PROPERTIES_DECODE_ERROR_ON_NULL_VALUES | CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS |                    \
+     CELIX_PROPERTIES_DECODE_ERROR_ON_UNSUPPORTED_ARRAYS | CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS)
+
+/**
+ * @brief Load properties from a stream.
+ *
+ * The stream is expected to be a valid readable stream and is not reset or closed by this function.
+ * The content of the stream is expected to be in the format of a JSON object.
+ *
+ * For decoding a single JSON object is decoded to a properties object.
+ *
+ * The keys of the JSON object are used as
+ * properties keys and the values of the JSON object are used as properties values. If there are nested
+ * JSON objects, the keys are concatenated with a '/' separator (e.g. `{"key": {"nested": "value"}}` will be
+ * decoded to a properties object with a single entry with key `key/nested` and (string) value `value`).
+ *
+ * Because properties keys are created by concatenating the JSON keys, there there could be collisions
+ * (e.g. `{"obj/key": "value", "obj": {"key": "value2"}}`, two entries with the key `obj/key`. In this case
+ * the last decoded JSON entry will be used.
+ *
+ * Properties entry types are determined by the JSON value type:
+ * - JSON string values are decoded as string properties entries.
+ * - JSON number values are decoded as long or double properties entries, depending on the value.
+ * - JSON boolean values are decoded as boolean properties entries.
+ * - jSON string values with a "version<" prefix and a ">" suffix are decoded as version properties entries (e.g.
+ * "version<1.2.3>").
+ * - JSON array values are decoded as array properties entries. The array can contain any of the above types, but mixed
+ * arrays are not supported.
+ * - JSON null values are ignored.
+ *
+ * For a overview of the possible decode flags, see the CELIX_PROPERTIES_DECODE_* flags documentation.
+ *
+ * @param[in] stream The input stream to parse.
+ * @param[in] decodeFlags The flags to use when decoding the input string.
+ * @param[out] out The properties object that will be created from the input string. The caller is responsible for
+ * freeing the returned properties object using celix_properties_destroy.
+ * @return CELIX_SUCCESS if the operation was successful, CELIX_ILLEGAL_ARGUMENT if the provided input cannot be
+ * decoded to a properties object and ENOMEM if there was not enough memory. CELIX_FILE_IO_EXCEPTION if the file
+ * could not be read.
+ */
+CELIX_UTILS_EXPORT celix_status_t celix_properties_loadFromStream(FILE* stream,
+                                                                  int decodeFlags,
+                                                                  celix_properties_t** out);
+
+/**
+ * @brief Load properties from a file.
+ *
+ * @warning The name is temporary and will be renamed to celix_properties_load in the future (when
+ * the current celix_properties_load is removed).
+ *
+ * The content of the filename file is expected to be in the format of a JSON object.
+ * For what can and cannot be parsed, see celix_properties_loadFromStream documentation.
+ *
+ * For a overview of the possible decode flags, see the CELIX_PROPERTIES_DECODE_* flags documentation.
+ *
+ * If an error occurs, the error status is returned and a message is logged to celix_err.
+ *
+ * @param[in] filename The file to load the properties from.
+ * @param[in] decodeFlags The flags to use when decoding the input string.
+ * @param[out] out The properties object that will be created from the input string. The caller is responsible for
+ * freeing the returned properties object using celix_properties_destroy.
+ * @return CELIX_SUCCESS if the operation was successful, CELIX_ILLEGAL_ARGUMENT if the provided input cannot be
+ * decoded to a properties object and ENOMEM if there was not enough memory. CELIX_FILE_IO_EXCEPTION if the file
+ * could not be opened.
+ */
+CELIX_UTILS_EXPORT celix_status_t celix_properties_load2(const char* filename,
+                                                         int decodeFlags,
+                                                         celix_properties_t** out);
+
+/**
+ * @brief Load properties from a string.
+ *
+ * @warning The name is temporary and will be renamed to celix_properties_loadFromString in the future (when
+ * the current celix_properties_loadFromString is removed).
+ *
+ * The input string is expected to be in the format of a JSON object.
+ * For what can and cannot be parsed, see celix_properties_loadFromStream documentation.
+ *
+ * For a overview of the possible decode flags, see the CELIX_PROPERTIES_DECODE_* flags documentation.
+ *
+ * If an error occurs, the error status is returned and a message is logged to celix_err.
+ *
+ * @param[in] input The input string to parse.
+ * @param[in] decodeFlags The flags to use when decoding the input string.
+ * @param[out] out The properties object that will be created from the input string. The caller is responsible for
+ * freeing the returned properties object using celix_properties_destroy.
+ * @return CELIX_SUCCESS if the operation was successful, CELIX_ILLEGAL_ARGUMENT if the provided input cannot be
+ * decoded to a properties object and ENOMEM if there was not enough memory.
+ */
+CELIX_UTILS_EXPORT celix_status_t celix_properties_loadFromString2(const char* input,
+                                                                   int decodeFlags,
+                                                                   celix_properties_t** out);
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/libs/utils/include/celix_utils.h b/libs/utils/include/celix_utils.h
index 7933b4e..19a4fd9 100644
--- a/libs/utils/include/celix_utils.h
+++ b/libs/utils/include/celix_utils.h
@@ -29,6 +29,8 @@
 #include <stdbool.h>
 
 #include "celix_utils_export.h"
+#include "celix_compiler.h"
+#include "celix_cleanup.h"
 
 #define CELIX_US_IN_SEC (1000000)
 #define CELIX_NS_IN_SEC ((CELIX_US_IN_SEC)*1000)
@@ -91,6 +93,59 @@
  */
 CELIX_UTILS_EXPORT void celix_utils_freeStringIfNotEqual(const char* buffer, char* str);
 
+/**
+ * @brief Guard for a string created with celix_utils_writeOrCreateString, celix_utils_writeOrCreateVString.
+ *
+ * Can be used with celix_auto() to automatically and correctly free the string.
+ * If the string is pointing to the buffer, the string should not be freed, otherwise the string should be freed.
+ */
+typedef struct celix_utils_string_guard {
+    const char* buffer;
+    char* string;
+} celix_utils_string_guard_t;
+
+/**
+ * @brief Initialize a guard for a string created with celix_utils_writeOrCreateString or
+ * celix_utils_writeOrCreateVString.
+ *
+ * De-initialize with celix_utils_stringGuard_deinit().
+ *
+ * No allocation is performed.
+ * This is intended to be used with celix_auto().
+ *
+ * * Example:
+ * ```
+ * const char* possibleLongString = ...
+ * char buffer[64];
+ * char* str = celix_utils_writeOrCreateString(buffer, sizeof(buffer), "Hello %s", possibleLongString);
+ * celix_auto(celix_utils_string_guard_t) strGuard = celix_utils_stringGuard_init(buffer, str);
+ * ```
+ * If the strGuard goes out of scope, the string will be freed correctly.
+ *
+ * @param buffer A (local) buffer which was potentially used to create the string.
+ * @param string The string to guard.
+ * @return An initialized string guard to be used with celix_auto().
+ */
+static CELIX_UNUSED inline celix_utils_string_guard_t celix_utils_stringGuard_init(const char* buffer, char* string) {
+    celix_utils_string_guard_t guard;
+    guard.buffer = buffer;
+    guard.string = string;
+    return guard;
+}
+
+/**
+ * @brief De-initialize a string guard.
+ *
+ * This will free the string if it is not equal to the buffer.
+ * This is intended to be used with celix_auto().
+ *
+ * @param guard The guard to de-initialize.
+ */
+static CELIX_UNUSED inline void celix_utils_stringGuard_deinit(celix_utils_string_guard_t* guard) {
+    celix_utils_freeStringIfNotEqual(guard->buffer, guard->string);
+}
+
+CELIX_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(celix_utils_string_guard_t, celix_utils_stringGuard_deinit)
 
 /**
  * @brief Compares two strings and returns true if the strings are equal.
diff --git a/libs/utils/src/celix_properties_private.h b/libs/utils/src/celix_properties_private.h
index 209b2ef..350a373 100644
--- a/libs/utils/src/celix_properties_private.h
+++ b/libs/utils/src/celix_properties_private.h
@@ -26,11 +26,13 @@
 #define CELIX_CELIX_PROPERTIES_PRIVATE_H
 
 #include "celix_properties.h"
+#include "jansson.h"
 
 #ifdef __cplusplus
 extern "C" {
 #endif
 
+
 /**
  * @brief Alloc new entry for the provided properties. Possible using the properties optimizer cache.
  */
@@ -41,6 +43,11 @@
  */
 char* celix_properties_createString(celix_properties_t* properties, const char* str);
 
+/**
+ * @brief Convert jansson error to celix status.
+ */
+celix_status_t celix_properties_jsonErrorToStatus(enum json_error_code error);
+
 
 #ifdef __cplusplus
 }
diff --git a/libs/utils/src/properties.c b/libs/utils/src/properties.c
index e00573a..5c8b657 100644
--- a/libs/utils/src/properties.c
+++ b/libs/utils/src/properties.c
@@ -334,6 +334,8 @@
             free(props);
             props = NULL;
         }
+    } else {
+        celix_err_push("Cannot allocate memory for properties");
     }
     return props;
 }
@@ -451,7 +453,6 @@
 
     celix_autoptr(celix_properties_t) props = celix_properties_create();
     if (!props) {
-        celix_err_push("Failed to create properties");
         return NULL;
     }
 
@@ -634,7 +635,7 @@
     return entry;
 }
 
-static const bool celix_properties_isEntryArrayListWithElType(const celix_properties_entry_t* entry,
+static bool celix_properties_isEntryArrayListWithElType(const celix_properties_entry_t* entry,
                                                               celix_array_list_element_type_t elType) {
     return entry != NULL && entry->valueType == CELIX_PROPERTIES_VALUE_TYPE_ARRAY_LIST &&
            celix_arrayList_getElementType(entry->typed.arrayValue) == elType;
diff --git a/libs/utils/src/properties_encoding.c b/libs/utils/src/properties_encoding.c
new file mode 100644
index 0000000..53af5ec
--- /dev/null
+++ b/libs/utils/src/properties_encoding.c
@@ -0,0 +1,644 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include "celix_properties.h"
+#include "celix_properties_private.h"
+
+#include "celix_err.h"
+#include "celix_stdlib_cleanup.h"
+#include "celix_utils.h"
+
+#include <assert.h>
+#include <jansson.h>
+#include <math.h>
+#include <string.h>
+
+#define CELIX_PROPERTIES_JSONPATH_SEPARATOR '.'
+
+static celix_status_t
+celix_properties_decodeValue(celix_properties_t* props, const char* key, json_t* jsonValue, int flags);
+
+static celix_status_t celix_properties_versionToJson(const celix_version_t* version, json_t** out) {
+    celix_autofree char* versionStr = celix_version_toString(version);
+    if (!versionStr) {
+        celix_err_push("Failed to create version string.");
+        return ENOMEM;
+    }
+    *out = json_sprintf("version<%s>", versionStr);
+    if (!*out) {
+        celix_err_push("Failed to create json string.");
+        return ENOMEM;
+    }
+    return CELIX_SUCCESS;
+}
+
+static celix_status_t celix_properties_arrayElementEntryValueToJson(celix_array_list_element_type_t elType,
+                                                                    celix_array_list_entry_t entry,
+                                                                    int flags,
+                                                                    json_t** out) {
+    *out = NULL;
+    switch (elType) {
+    case CELIX_ARRAY_LIST_ELEMENT_TYPE_STRING:
+        *out = json_string(entry.stringVal);
+        break;
+    case CELIX_ARRAY_LIST_ELEMENT_TYPE_LONG:
+        *out = json_integer(entry.longVal);
+        break;
+    case CELIX_ARRAY_LIST_ELEMENT_TYPE_DOUBLE:
+        if (isnan(entry.doubleVal) || isinf(entry.doubleVal)) {
+            if (flags & CELIX_PROPERTIES_ENCODE_ERROR_ON_NAN_INF) {
+                celix_err_push("Invalid NaN or Inf.");
+                return CELIX_ILLEGAL_ARGUMENT;
+            }
+            return CELIX_SUCCESS; // ignore NaN and Inf
+        }
+        *out = json_real(entry.doubleVal);
+        break;
+    case CELIX_ARRAY_LIST_ELEMENT_TYPE_BOOL:
+        *out = json_boolean(entry.boolVal);
+        break;
+    case CELIX_ARRAY_LIST_ELEMENT_TYPE_VERSION:
+        return celix_properties_versionToJson(entry.versionVal, out);
+    default:
+        // LCOV_EXCL_START
+        celix_err_pushf("Invalid array list element type %d.", elType);
+        return CELIX_ILLEGAL_ARGUMENT;
+        // LCOV_EXCL_STOP
+    }
+    if (!*out) {
+        celix_err_push("Failed to create json value.");
+        return ENOMEM;
+    }
+    return CELIX_SUCCESS;
+}
+
+static celix_status_t celix_properties_arrayEntryValueToJson(const char* key,
+                                                             const celix_properties_entry_t* entry,
+                                                             int flags,
+                                                             json_t** out) {
+    *out = NULL;
+    if (celix_arrayList_size(entry->typed.arrayValue) == 0) {
+        if (flags & CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS) {
+            celix_err_pushf("Invalid empty array for key %s.", key);
+            return CELIX_ILLEGAL_ARGUMENT;
+        }
+        return CELIX_SUCCESS; // empty array -> treat as unset property
+    }
+
+    json_auto_t* array = json_array();
+    if (!array) {
+        celix_err_push("Failed to create json array.");
+        return ENOMEM;
+    }
+
+    int size = celix_arrayList_size(entry->typed.arrayValue);
+    celix_array_list_element_type_t elType = celix_arrayList_getElementType(entry->typed.arrayValue);
+    for (int i = 0; i < size; ++i) {
+        celix_array_list_entry_t arrayEntry = celix_arrayList_getEntry(entry->typed.arrayValue, i);
+        json_t* jsonValue;
+        celix_status_t status = celix_properties_arrayElementEntryValueToJson(elType, arrayEntry, flags, &jsonValue);
+        if (status != CELIX_SUCCESS) {
+            celix_err_pushf("Failed to encode array element(%d) for key %s.", i, key);
+            return status;
+        } else if (!jsonValue) {
+            // ignore unset values
+        } else {
+            int rc = json_array_append_new(array, jsonValue);
+            if (rc != 0) {
+                celix_err_push("Failed to append json string to array.");
+                return ENOMEM;
+            }
+        }
+    }
+
+    if (json_array_size(array) == 0) {
+        if (flags & CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS) {
+            celix_err_pushf("Invalid empty array for key %s.", key);
+            return CELIX_ILLEGAL_ARGUMENT;
+        }
+        return CELIX_SUCCESS; // empty array -> treat as unset property
+    }
+
+    *out = celix_steal_ptr(array);
+    return CELIX_SUCCESS;
+}
+
+static celix_status_t
+celix_properties_entryValueToJson(const char* key, const celix_properties_entry_t* entry, int flags, json_t** out) {
+    *out = NULL;
+    switch (entry->valueType) {
+    case CELIX_PROPERTIES_VALUE_TYPE_STRING:
+        *out = json_string(entry->value);
+        break;
+    case CELIX_PROPERTIES_VALUE_TYPE_LONG:
+        *out = json_integer(entry->typed.longValue);
+        break;
+    case CELIX_PROPERTIES_VALUE_TYPE_DOUBLE:
+        if (isnan(entry->typed.doubleValue) || isinf(entry->typed.doubleValue)) {
+            if (flags & CELIX_PROPERTIES_ENCODE_ERROR_ON_NAN_INF) {
+                celix_err_pushf("Invalid NaN or Inf in key '%s'.", key);
+                return CELIX_ILLEGAL_ARGUMENT;
+            }
+            return CELIX_SUCCESS; // ignore NaN and Inf
+        }
+        *out = json_real(entry->typed.doubleValue);
+        break;
+    case CELIX_PROPERTIES_VALUE_TYPE_BOOL:
+        *out = json_boolean(entry->typed.boolValue);
+        break;
+    case CELIX_PROPERTIES_VALUE_TYPE_VERSION:
+        return celix_properties_versionToJson(entry->typed.versionValue, out);
+    case CELIX_PROPERTIES_VALUE_TYPE_ARRAY_LIST:
+        return celix_properties_arrayEntryValueToJson(key, entry, flags, out);
+    default:
+        // LCOV_EXCL_START
+        celix_err_pushf("Unexpected properties entry type %d.", entry->valueType);
+        return CELIX_ILLEGAL_ARGUMENT;
+        // LCOV_EXCL_STOP
+    }
+
+    if (!*out) {
+        celix_err_pushf("Failed to create json value for key '%s'.", key);
+        return ENOMEM;
+    }
+    return CELIX_SUCCESS;
+}
+
+static celix_status_t celix_properties_addJsonValueToJson(json_t* value, const char* key, json_t* obj, int flags) {
+    if (!value) {
+        // ignore unset values
+        return CELIX_SUCCESS;
+    }
+
+    json_t* field = json_object_get(obj, key);
+    if (field) {
+        if (flags & CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS) {
+            celix_err_pushf("Invalid key collision. key '%s' already exists.", key);
+            json_decref(value);
+            return CELIX_ILLEGAL_ARGUMENT;
+        }
+    }
+
+    int rc = json_object_set_new(obj, key, value);
+    if (rc != 0) {
+        celix_err_push("Failed to set json object");
+        return ENOMEM;
+    }
+    return CELIX_SUCCESS;
+}
+
+static celix_status_t celix_properties_addPropertiesEntryFlatToJson(const celix_properties_entry_t* entry,
+                                                                    const char* key,
+                                                                    json_t* root,
+                                                                    int flags) {
+    json_t* value;
+    celix_status_t status = celix_properties_entryValueToJson(key, entry, flags, &value);
+    status = CELIX_DO_IF(status, celix_properties_addJsonValueToJson(value, key, root, flags));
+    return status;
+}
+
+static celix_status_t celix_properties_addPropertiesEntryToJson(const celix_properties_entry_t* entry,
+                                                                const char* key,
+                                                                json_t* root,
+                                                                int flags) {
+    json_t* jsonObj = root;
+    const char* fieldName = key;
+    const char* slash = strchr(key, CELIX_PROPERTIES_JSONPATH_SEPARATOR);
+    while (slash) {
+        char buf[64];
+        char* name = celix_utils_writeOrCreateString(buf, sizeof(buf), "%.*s", (int)(slash - fieldName), fieldName);
+        celix_auto(celix_utils_string_guard_t) strGuard = celix_utils_stringGuard_init(buf, name);
+        if (!name) {
+            celix_err_push("Failed to create name string");
+            return ENOMEM;
+        }
+        json_t* subObj = json_object_get(jsonObj, name);
+        if (subObj && !json_is_object(subObj)) {
+            if (flags & CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS) {
+                celix_err_pushf("Invalid key collision. Key '%s' already exists.", name);
+                return CELIX_ILLEGAL_ARGUMENT;
+            }
+            return CELIX_SUCCESS;
+        }
+        if (!subObj) {
+            subObj = json_object();
+            if (!subObj) {
+                celix_err_push("Failed to create json object");
+                return ENOMEM;
+            }
+            int rc = json_object_set_new(jsonObj, name, subObj);
+            if (rc != 0) {
+                celix_err_push("Failed to set json object");
+                return ENOMEM;
+            }
+        }
+
+        jsonObj = subObj;
+        fieldName = slash + 1;
+        slash = strchr(fieldName, CELIX_PROPERTIES_JSONPATH_SEPARATOR);
+    }
+
+    json_t* value;
+    celix_status_t status = celix_properties_entryValueToJson(fieldName, entry, flags, &value);
+    if (status != CELIX_SUCCESS) {
+        return status;
+    }
+    return celix_properties_addJsonValueToJson(value, fieldName, jsonObj, flags);
+}
+
+celix_status_t celix_properties_saveToStream(const celix_properties_t* properties, FILE* stream, int encodeFlags) {
+    json_auto_t* root = json_object();
+    if (!root) {
+        celix_err_push("Failed to create json object");
+        return ENOMEM;
+    }
+
+    if (!(encodeFlags & CELIX_PROPERTIES_ENCODE_FLAT_STYLE) && !(encodeFlags & CELIX_PROPERTIES_ENCODE_NESTED_STYLE)) {
+        //no encoding flags set, default to flat
+        encodeFlags |= CELIX_PROPERTIES_ENCODE_FLAT_STYLE;
+    }
+
+    CELIX_PROPERTIES_ITERATE(properties, iter) {
+        celix_status_t status;
+        if (encodeFlags & CELIX_PROPERTIES_ENCODE_FLAT_STYLE) {
+            status = celix_properties_addPropertiesEntryFlatToJson(&iter.entry, iter.key, root, encodeFlags);
+        } else {
+            assert(encodeFlags & CELIX_PROPERTIES_ENCODE_NESTED_STYLE);
+            status = celix_properties_addPropertiesEntryToJson(&iter.entry, iter.key, root, encodeFlags);
+        }
+        if (status != CELIX_SUCCESS) {
+            return status;
+        }
+    }
+
+    size_t jsonFlags = JSON_COMPACT;
+    if (encodeFlags & CELIX_PROPERTIES_ENCODE_PRETTY) {
+        jsonFlags = JSON_INDENT(2);
+    }
+
+    int rc = json_dumpf(root, stream, jsonFlags);
+    if (rc != 0) {
+        celix_err_push("Failed to dump json object to stream.");
+        return CELIX_FILE_IO_EXCEPTION;
+    }
+    return CELIX_SUCCESS;
+}
+
+celix_status_t celix_properties_save(const celix_properties_t* properties, const char* filename, int encodeFlags) {
+    FILE* stream = fopen(filename, "w");
+    if (!stream) {
+        celix_err_pushf("Failed to open file %s.", filename);
+        return CELIX_FILE_IO_EXCEPTION;
+    }
+    celix_status_t status = celix_properties_saveToStream(properties, stream, encodeFlags);
+    int rc = fclose(stream);
+    if (rc != 0) {
+        celix_err_pushf("Failed to close file %s: %s", filename, strerror(errno));
+        return CELIX_FILE_IO_EXCEPTION;
+    }
+    return status;
+}
+
+celix_status_t celix_properties_saveToString(const celix_properties_t* properties, int encodeFlags, char** out) {
+    *out = NULL;
+    celix_autofree char* buffer = NULL;
+    size_t size = 0;
+    FILE* stream = open_memstream(&buffer, &size);
+    if (!stream) {
+        celix_err_push("Failed to open memstream.");
+        return ENOMEM;
+    }
+
+    celix_status_t status = celix_properties_saveToStream(properties, stream, encodeFlags);
+    (void)fclose(stream);
+    if (!buffer || status != CELIX_SUCCESS) {
+        if (!buffer || status == CELIX_FILE_IO_EXCEPTION) {
+            return ENOMEM; // Using memstream as stream, return ENOMEM instead of CELIX_FILE_IO_EXCEPTION
+        }
+        return status;
+    }
+    *out = celix_steal_ptr(buffer);
+    return CELIX_SUCCESS;
+}
+
+static celix_status_t celix_properties_parseVersion(const char* value, celix_version_t** out) {
+    // precondition: value is a valid version string (8 chars prefix and 1 char suffix)
+    *out = NULL;;
+    char buf[32];
+    char* extracted = celix_utils_writeOrCreateString(buf, sizeof(buf), "%.*s", (int)strlen(value) - 9, value + 8);
+    celix_auto(celix_utils_string_guard_t) guard = celix_utils_stringGuard_init(buf, extracted);
+    if (!extracted) {
+        celix_err_push("Failed to create extracted version string.");
+        return ENOMEM;
+    }
+    celix_status_t status = celix_version_parse(extracted, out);
+    if (status != CELIX_SUCCESS) {
+        celix_err_push("Failed to parse version string.");
+        return status;
+    }
+    return CELIX_SUCCESS;
+}
+
+static bool celix_properties_isVersionString(const char* value) {
+    return strncmp(value, "version<", 8) == 0 && value[strlen(value) - 1] == '>';
+}
+
+/**
+ * @brief Determine the array list element type based on the json value.
+ *
+ * If the array is of a mixed type, the element type cannot be determined and a CELIX_ILLEGAL_ARGUMENT is
+ * returned.
+ *
+ * @param[in] value The json value.
+ * @param[out] out The array list element type.
+ * @return CELIX_SUCCESS if the array list element type could be determined or CELIX_ILLEGAL_ARGUMENT if the array
+ * type could not be determined.
+ */
+static celix_status_t celix_properties_determineArrayType(const json_t* jsonArray,
+                                                          celix_array_list_element_type_t* out) {
+    assert(json_array_size(jsonArray) > 0); //precondition: size > 0
+
+    json_t* value;
+    int index;
+    json_type type = JSON_NULL;
+    bool versionType = false;
+    json_array_foreach(jsonArray, index, value) {
+        if (index == 0) {
+            type = json_typeof(value);
+            if (type == JSON_STRING && celix_properties_isVersionString(json_string_value(value))) {
+                versionType = true;
+            }
+        } else if ((type == JSON_TRUE || type == JSON_FALSE) && json_is_boolean(value)) {
+            // bool, ok.
+            continue;
+        } else if (type == JSON_INTEGER && json_typeof(value) == JSON_REAL) {
+            // mixed integer and real, ok but promote to real
+            type = JSON_REAL;
+            continue;
+        } else if (type == JSON_REAL && json_typeof(value) == JSON_INTEGER) {
+            // mixed real and integer, ok
+            continue;
+        } else if (type != json_typeof(value)) {
+            return CELIX_ILLEGAL_ARGUMENT;
+        } else if (versionType) {
+            if (json_typeof(value) != JSON_STRING || !celix_properties_isVersionString(json_string_value(value))) {
+                return CELIX_ILLEGAL_ARGUMENT;
+            }
+        }
+    }
+
+    switch (type) {
+    case JSON_STRING:
+        *out = versionType ? CELIX_ARRAY_LIST_ELEMENT_TYPE_VERSION : CELIX_ARRAY_LIST_ELEMENT_TYPE_STRING;
+        break;
+    case JSON_INTEGER:
+        *out = CELIX_ARRAY_LIST_ELEMENT_TYPE_LONG;
+        break;
+    case JSON_REAL:
+        *out = CELIX_ARRAY_LIST_ELEMENT_TYPE_DOUBLE;
+        break;
+    case JSON_TRUE:
+    case JSON_FALSE:
+        *out = CELIX_ARRAY_LIST_ELEMENT_TYPE_BOOL;
+        break;
+    default:
+        //JSON_NULL, JSON_OBJECT and  JSON_ARRAY
+        return CELIX_ILLEGAL_ARGUMENT;
+    }
+
+    return CELIX_SUCCESS;
+}
+
+static celix_status_t
+celix_properties_decodeArray(celix_properties_t* props, const char* key, const json_t* jsonArray, int flags) {
+    celix_array_list_element_type_t elType;
+    celix_status_t status = celix_properties_determineArrayType(jsonArray, &elType);
+    if (status != CELIX_SUCCESS && (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_UNSUPPORTED_ARRAYS)) {
+        celix_autofree char* arrStr = json_dumps(jsonArray, JSON_ENCODE_ANY);
+        celix_err_pushf("Invalid mixed, null, object or multidimensional array for key '%s': %s.", key, arrStr);
+        return status;
+    } else if (status != CELIX_SUCCESS) {
+        //ignore mixed types
+        return CELIX_SUCCESS;
+    }
+
+    celix_array_list_create_options_t opts = CELIX_EMPTY_ARRAY_LIST_CREATE_OPTIONS;
+    opts.elementType = elType;
+    celix_autoptr(celix_array_list_t) array = celix_arrayList_createWithOptions(&opts);
+    if (!array) {
+        return ENOMEM;
+    }
+
+    json_t* value;
+    int index;
+    json_array_foreach(jsonArray, index, value) {
+        switch (elType) {
+        case CELIX_ARRAY_LIST_ELEMENT_TYPE_STRING:
+            status = celix_arrayList_addString(array, json_string_value(value));
+            break;
+        case CELIX_ARRAY_LIST_ELEMENT_TYPE_LONG:
+            status = celix_arrayList_addLong(array, (long)json_integer_value(value));
+            break;
+        case CELIX_ARRAY_LIST_ELEMENT_TYPE_DOUBLE:
+            status = celix_arrayList_addDouble(array, json_number_value(value));
+            break;
+        case CELIX_ARRAY_LIST_ELEMENT_TYPE_BOOL:
+            status = celix_arrayList_addBool(array, json_boolean_value(value));
+            break;
+        case CELIX_ARRAY_LIST_ELEMENT_TYPE_VERSION: {
+            celix_version_t* v;
+            status = celix_properties_parseVersion(json_string_value(value), &v);
+            status = CELIX_DO_IF(status, celix_arrayList_assignVersion(array, v));
+            break;
+        }
+        default:
+            // LCOV_EXCL_START
+            celix_err_pushf("Unexpected array list element type %d for key %s.", elType, key);
+            return CELIX_ILLEGAL_ARGUMENT;
+            // LCOV_EXCL_STOP
+        }
+        if (status != CELIX_SUCCESS) {
+            return status;
+        }
+    }
+    return celix_properties_assignArrayList(props, key, celix_steal_ptr(array));
+}
+
+static celix_status_t
+celix_properties_decodeValue(celix_properties_t* props, const char* key, json_t* jsonValue, int flags) {
+    if (strncmp(key, "", 1) == 0) {
+        if (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS) {
+            celix_err_push("Key cannot be empty.");
+            return CELIX_ILLEGAL_ARGUMENT;
+        }
+    }
+
+    if (!json_is_object(jsonValue) && celix_properties_hasKey(props, key) &&
+        (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_COLLISIONS)) {
+        celix_err_pushf("Invalid key collision. Key '%s' already exists.", key);
+        return CELIX_ILLEGAL_ARGUMENT;
+    }
+
+    celix_status_t status = CELIX_SUCCESS;
+    if (json_is_string(jsonValue) && celix_properties_isVersionString(json_string_value(jsonValue))) {
+        celix_version_t* version;
+        status = celix_properties_parseVersion(json_string_value(jsonValue), &version);
+        status = CELIX_DO_IF(status, celix_properties_assignVersion(props, key, version));
+    } else if (json_is_string(jsonValue)) {
+        status = celix_properties_setString(props, key, json_string_value(jsonValue));
+    } else if (json_is_integer(jsonValue)) {
+        status = celix_properties_setLong(props, key, json_integer_value(jsonValue));
+    } else if (json_is_real(jsonValue)) {
+        status = celix_properties_setDouble(props, key, json_real_value(jsonValue));
+    } else if (json_is_boolean(jsonValue)) {
+        status = celix_properties_setBool(props, key, json_boolean_value(jsonValue));
+    } else if (json_is_object(jsonValue)) {
+        const char* fieldName;
+        json_t* fieldValue;
+        json_object_foreach(jsonValue, fieldName, fieldValue) {
+            char buf[64];
+            char* combinedKey = celix_utils_writeOrCreateString(buf, sizeof(buf), "%s%c%s", key, CELIX_PROPERTIES_JSONPATH_SEPARATOR, fieldName);
+            celix_auto(celix_utils_string_guard_t) strGuard = celix_utils_stringGuard_init(buf, combinedKey);
+            if (!combinedKey) {
+                celix_err_push("Failed to create sub key.");
+                return ENOMEM;
+            }
+            status = celix_properties_decodeValue(props, combinedKey, fieldValue, flags);
+            if (status != CELIX_SUCCESS) {
+                return status;
+            }
+        }
+        return CELIX_SUCCESS;
+    } else if (json_is_array(jsonValue) && json_array_size(jsonValue) == 0) {
+        if (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS) {
+            celix_err_pushf("Invalid empty array for key '%s'.", key);
+            return CELIX_ILLEGAL_ARGUMENT;
+        }
+        // ignore empty arrays
+        return CELIX_SUCCESS;
+    } else if (json_is_array(jsonValue)) {
+        status = celix_properties_decodeArray(props, key, jsonValue, flags);
+    } else if (json_is_null(jsonValue)) {
+        if (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_NULL_VALUES) {
+            celix_err_pushf("Invalid null value for key '%s'.", key);
+            return CELIX_ILLEGAL_ARGUMENT;
+        }
+        // ignore null values
+        return CELIX_SUCCESS;
+    } else {
+        // LCOV_EXCL_START
+        celix_err_pushf("Unexpected json value type for key '%s'.", key);
+        return CELIX_ILLEGAL_ARGUMENT;
+        // LCOV_EXCL_STOP
+    }
+    return status;
+}
+
+static celix_status_t celix_properties_decodeFromJson(json_t* obj, int flags, celix_properties_t** out) {
+    *out = NULL;
+    if (!json_is_object(obj)) {
+        celix_err_push("Expected json object.");
+        return CELIX_ILLEGAL_ARGUMENT;
+    }
+
+    celix_autoptr(celix_properties_t) props = celix_properties_create();
+    if (!props) {
+        return ENOMEM;
+    }
+
+    const char* key;
+    json_t* value;
+    json_object_foreach(obj, key, value) {
+        celix_status_t status = celix_properties_decodeValue(props, key, value, flags);
+        if (status != CELIX_SUCCESS) {
+            return status;
+        }
+    }
+
+    *out = celix_steal_ptr(props);
+    return CELIX_SUCCESS;
+}
+
+celix_status_t celix_properties_loadFromStream(FILE* stream, int decodeFlags, celix_properties_t** out) {
+    json_error_t jsonError;
+    size_t jsonFlags = 0;
+    if (decodeFlags & CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES) {
+        jsonFlags = JSON_REJECT_DUPLICATES;
+    }
+    json_auto_t* root = json_loadf(stream, jsonFlags, &jsonError);
+    if (!root) {
+        celix_err_pushf("Failed to parse json from %s:%i:%i: %s.",
+                        jsonError.source,
+                        jsonError.line,
+                        jsonError.column,
+                        jsonError.text);
+        return celix_properties_jsonErrorToStatus(json_error_code(&jsonError));
+    }
+    return celix_properties_decodeFromJson(root, decodeFlags, out);
+}
+
+celix_status_t celix_properties_load2(const char* filename, int decodeFlags, celix_properties_t** out) {
+    FILE* stream = fopen(filename, "r");
+    if (!stream) {
+        celix_err_pushf("Failed to open file %s.", filename);
+        return CELIX_FILE_IO_EXCEPTION;
+    }
+    celix_status_t status = celix_properties_loadFromStream(stream, decodeFlags, out);
+    fclose(stream);
+    return status;
+}
+
+celix_status_t celix_properties_loadFromString2(const char* input, int decodeFlags, celix_properties_t** out) {
+    FILE* stream = fmemopen((void*)input, strlen(input), "r");
+    if (!stream) {
+        celix_err_push("Failed to open memstream.");
+        return ENOMEM;
+    }
+    celix_status_t status = celix_properties_loadFromStream(stream, decodeFlags, out);
+    fclose(stream);
+    return status;
+}
+
+celix_status_t celix_properties_jsonErrorToStatus(enum json_error_code error) {
+    switch (error) {
+    case json_error_unknown:
+        return CELIX_ILLEGAL_STATE;
+    case json_error_out_of_memory:
+    case json_error_stack_overflow:
+        return ENOMEM;
+    case json_error_cannot_open_file:
+        return CELIX_FILE_IO_EXCEPTION;
+    case json_error_invalid_argument:
+    case json_error_invalid_utf8:
+    case json_error_premature_end_of_input:
+    case json_error_end_of_input_expected:
+    case json_error_invalid_syntax:
+    case json_error_invalid_format:
+    case json_error_wrong_type:
+    case json_error_null_character:
+    case json_error_null_value:
+    case json_error_null_byte_in_key:
+    case json_error_duplicate_key:;
+    case json_error_numeric_overflow:
+    case json_error_item_not_found:
+    case json_error_index_out_of_range:
+    default:
+        return CELIX_ILLEGAL_ARGUMENT;
+    }
+}