diff --git a/include/libcamera/internal/yaml_parser.h b/include/libcamera/internal/yaml_parser.h
index 16708e488d88..6211ff4ae563 100644
--- a/include/libcamera/internal/yaml_parser.h
+++ b/include/libcamera/internal/yaml_parser.h
@@ -159,6 +159,14 @@ public:
 	{
 		return type_ == Type::Dictionary;
 	}
+	bool isEmpty() const
+	{
+		return type_ == Type::Empty;
+	}
+	explicit operator bool() const
+	{
+		return type_ != Type::Empty;
+	}
 
 	std::size_t size() const;
 
@@ -212,6 +220,7 @@ private:
 		Dictionary,
 		List,
 		Value,
+		Empty,
 	};
 
 	template<typename T>
diff --git a/src/libcamera/yaml_parser.cpp b/src/libcamera/yaml_parser.cpp
index 8b6a403898be..4784f2dc3d62 100644
--- a/src/libcamera/yaml_parser.cpp
+++ b/src/libcamera/yaml_parser.cpp
@@ -38,12 +38,12 @@ static const YamlObject empty;
  * \brief A class representing the tree structure of the YAML content
  *
  * The YamlObject class represents the tree structure of YAML content. A
- * YamlObject can be a dictionary or list of YamlObjects or a value if a tree
- * leaf.
+ * YamlObject can be empty, a dictionary or list of YamlObjects, or a value if a
+ * tree leaf.
  */
 
 YamlObject::YamlObject()
-	: type_(Type::Value)
+	: type_(Type::Empty)
 {
 }
 
@@ -70,6 +70,20 @@ YamlObject::~YamlObject() = default;
  * \return True if the YamlObject is a dictionary, false otherwise
  */
 
+/**
+ * \fn YamlObject::isEmpty()
+ * \brief Return whether the YamlObject is an empty
+ *
+ * \return True if the YamlObject is empty, false otherwise
+ */
+
+/**
+ * \fn YamlObject::operator bool()
+ * \brief Return whether the YamlObject is a non-empty
+ *
+ * \return False if the YamlObject is empty, true otherwise
+ */
+
 /**
  * \fn YamlObject::size()
  * \brief Retrieve the number of elements in a dictionary or list YamlObject
@@ -443,7 +457,8 @@ template std::optional<std::vector<Size>> YamlObject::getList<Size>() const;
  *
  * This function retrieves an element of the YamlObject. Only YamlObject
  * instances of List type associate elements with index, calling this function
- * on other types of instances is invalid and results in undefined behaviour.
+ * on other types of instances or with an invalid index results in an empty
+ * object.
  *
  * \return The YamlObject as an element of the list
  */
@@ -480,8 +495,8 @@ bool YamlObject::contains(const std::string &key) const
  *
  * This function retrieve a member of a YamlObject by name. Only YamlObject
  * instances of Dictionary type associate elements with names, calling this
- * function on other types of instances is invalid and results in undefined
- * behaviour.
+ * function on other types of instances or with a nonexistent key results in an
+ * empty object.
  *
  * \return The YamlObject corresponding to the \a key member
  */
diff --git a/test/yaml-parser.cpp b/test/yaml-parser.cpp
index 81c829834667..9d340e29d4a9 100644
--- a/test/yaml-parser.cpp
+++ b/test/yaml-parser.cpp
@@ -34,10 +34,12 @@ static const string testYaml =
 	"list:\n"
 	"  - James\n"
 	"  - Mary\n"
+	"  - \n"
 	"dictionary:\n"
 	"  a: 1\n"
 	"  c: 3\n"
 	"  b: 2\n"
+	"  empty:\n"
 	"level1:\n"
 	"  level2:\n"
 	"    - [1, 2]\n"
@@ -430,9 +432,10 @@ protected:
 		if (testObjectType(listObj, "list", Type::List) != TestPass)
 			return TestFail;
 
-		static constexpr std::array<const char *, 2> listValues{
+		static constexpr std::array<const char *, 3> listValues{
 			"James",
 			"Mary",
+			"",
 		};
 
 		if (listObj.size() != listValues.size()) {
@@ -465,16 +468,22 @@ protected:
 			i++;
 		}
 
+		/* Ensure that empty objects get parsed as empty strings. */
+		if (!listObj[2].isValue()) {
+			cerr << "Empty object is not a value" << std::endl;
+		}
+
 		/* Test dictionary object */
 		auto &dictObj = (*root)["dictionary"];
 
 		if (testObjectType(dictObj, "dictionary", Type::Dictionary) != TestPass)
 			return TestFail;
 
-		static constexpr std::array<std::pair<const char *, int>, 3> dictValues{ {
+		static constexpr std::array<std::pair<const char *, int>, 4> dictValues{ {
 			{ "a", 1 },
 			{ "c", 3 },
 			{ "b", 2 },
+			{ "empty", -100 },
 		} };
 
 		size_t dictSize = dictValues.size();
@@ -505,7 +514,7 @@ protected:
 				return TestFail;
 			}
 
-			if (elem.get<int32_t>(0) != item.second) {
+			if (elem.get<int32_t>(-100) != item.second) {
 				std::cerr << "Dictionary element " << i << " has wrong value"
 					  << std::endl;
 				return TestFail;
@@ -514,6 +523,42 @@ protected:
 			i++;
 		}
 
+		/* Ensure that empty objects get parsed as empty strings. */
+		if (!dictObj["empty"].isValue()) {
+			cerr << "Empty object is not of type value" << std::endl;
+			return TestFail;
+		}
+
+		/* Ensure that keys without values are still added to a dict. */
+		if (!dictObj.contains("empty")) {
+			cerr << "Empty element is missing in dict" << std::endl;
+			return TestFail;
+		}
+
+		/* Test explicit cast to bool on an empty object must return true. */
+		if (!!dictObj["empty"] != true) {
+			cerr << "Casting empty entry to bool returns false" << std::endl;
+			return TestFail;
+		}
+
+		/* Test nonexistent object has value type empty. */
+		if (!dictObj["nonexistent"].isEmpty()) {
+			cerr << "Accessing nonexistent object returns non-empty object" << std::endl;
+			return TestFail;
+		}
+
+		/* Test access to nonexistent member. */
+		if (dictObj["nonexistent"].get<std::string>("default") != "default") {
+			cerr << "Accessing nonexistent dict entry fails to return default" << std::endl;
+			return TestFail;
+		}
+
+		/* Test explicit cast to bool on nonexistent object returns false. */
+		if (!!dictObj["nonexistent"] != false) {
+			cerr << "Casting nonexistent dict entry to bool returns true" << std::endl;
+			return TestFail;
+		}
+
 		/* Make sure utils::map_keys() works on the adapter. */
 		(void)utils::map_keys(dictObj.asDict());
 
