diff --git a/Changelog.md b/Changelog.md
index 534bce5..72f34d2 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -3,6 +3,9 @@
## Version 14.1.6
- Add optional `expireDate` parameter to `doShare` / `doShareAsync`, so an
expiration date can be set when creating a share (issue #76)
+- Fix JSON parsing of empty OCS results: the API serializes empty collections
+ as `[]` instead of `{}`, which caused a `MismatchedInputException` when e.g.
+ listing users or groups on an empty result (issue #112)
- Testing: integration tests can now auto-provision a throw-away Nextcloud
server via Testcontainers when Docker is available, and are executed in CI
(GitHub Actions)
diff --git a/src/main/java/org/aarboard/nextcloud/api/utils/JsonAnswerParser.java b/src/main/java/org/aarboard/nextcloud/api/utils/JsonAnswerParser.java
index c514d4a..1466d4b 100644
--- a/src/main/java/org/aarboard/nextcloud/api/utils/JsonAnswerParser.java
+++ b/src/main/java/org/aarboard/nextcloud/api/utils/JsonAnswerParser.java
@@ -19,6 +19,11 @@ public class JsonAnswerParser implements ConnectorCommon.R
private JsonAnswerParser(Class answerClass) {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enable(DeserializationFeature.UNWRAP_ROOT_VALUE);
+ // The OCS API serializes empty PHP associative arrays as a JSON array
+ // "[]" instead of an object "{}". Treat such empty arrays as a null
+ // object so answers with no data (e.g. an empty user/group list)
+ // deserialize instead of throwing a MismatchedInputException.
+ objectMapper.enable(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT);
objectReader = objectMapper.readerFor(answerClass);
}
diff --git a/src/test/java/org/aarboard/nextcloud/api/utils/JsonAnswerParserTest.java b/src/test/java/org/aarboard/nextcloud/api/utils/JsonAnswerParserTest.java
new file mode 100644
index 0000000..8cbeaa1
--- /dev/null
+++ b/src/test/java/org/aarboard/nextcloud/api/utils/JsonAnswerParserTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2026 a.schild
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.aarboard.nextcloud.api.utils;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.io.StringReader;
+import java.util.List;
+
+import org.aarboard.nextcloud.api.provisioning.GroupListAnswer;
+import org.aarboard.nextcloud.api.provisioning.UserDetailsListAnswer;
+import org.aarboard.nextcloud.api.provisioning.UserListAnswer;
+import org.junit.Test;
+
+/**
+ * Pure parsing tests (no server required) for the OCS JSON quirk where empty
+ * collections are serialized as an empty array {@code []} instead of an object.
+ *
+ * @author a.schild
+ */
+public class JsonAnswerParserTest {
+
+ /**
+ * An empty group list comes back with {@code "data":[]} instead of an
+ * object; it must deserialize to an empty list rather than throwing.
+ */
+ @Test
+ public void testEmptyGroupListDataAsArray() {
+ String json = "{\"ocs\":{\"meta\":{\"status\":\"ok\",\"statuscode\":100},\"data\":[]}}";
+ GroupListAnswer answer = JsonAnswerParser.getInstance(GroupListAnswer.class)
+ .parseResponse(new StringReader(json));
+ assertTrue(answer.getAllGroups().isEmpty());
+ }
+
+ /**
+ * An empty user-details map comes back with {@code "users":[]}; it must
+ * deserialize to an empty list rather than throwing.
+ */
+ @Test
+ public void testEmptyUserDetailsUsersAsArray() {
+ String json = "{\"ocs\":{\"meta\":{\"status\":\"ok\",\"statuscode\":100},\"data\":{\"users\":[]}}}";
+ UserDetailsListAnswer answer = JsonAnswerParser.getInstance(UserDetailsListAnswer.class)
+ .parseResponse(new StringReader(json));
+ assertTrue(answer.getAllUserDetails().isEmpty());
+ }
+
+ /**
+ * An empty user list comes back with {@code "data":[]}; it must deserialize
+ * to an empty list rather than throwing.
+ */
+ @Test
+ public void testEmptyUserListDataAsArray() {
+ String json = "{\"ocs\":{\"meta\":{\"status\":\"ok\",\"statuscode\":100},\"data\":[]}}";
+ UserListAnswer answer = JsonAnswerParser.getInstance(UserListAnswer.class)
+ .parseResponse(new StringReader(json));
+ assertTrue(answer.getAllUsers().isEmpty());
+ }
+
+ /**
+ * A populated group list must still deserialize correctly (guards against
+ * the empty-array handling breaking the normal case).
+ */
+ @Test
+ public void testPopulatedGroupList() {
+ String json = "{\"ocs\":{\"meta\":{\"status\":\"ok\",\"statuscode\":100},"
+ + "\"data\":{\"groups\":[\"admin\",\"users\"]}}}";
+ GroupListAnswer answer = JsonAnswerParser.getInstance(GroupListAnswer.class)
+ .parseResponse(new StringReader(json));
+ List groups = answer.getAllGroups();
+ assertEquals(2, groups.size());
+ assertEquals("admin", groups.get(0));
+ assertEquals("users", groups.get(1));
+ }
+}