From b090bcde9db459eada7a2f662f56a5a0c0536da9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Schild?= Date: Fri, 24 Jul 2026 10:52:20 +0200 Subject: [PATCH] Accept OCS empty arrays as null objects when parsing JSON (issue #112) The OCS API serializes an empty PHP associative array as a JSON array "[]" rather than an object "{}". When a result was empty (e.g. an empty user or group list), fields typed as objects (GroupListAnswer.Data, UserDetailsListAnswer.Users, User) received "[]" and Jackson threw a MismatchedInputException. Enable DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT on the shared ObjectMapper so such empty arrays deserialize to null; the answer classes already treat null as an empty result. Adds JsonAnswerParserTest, a server-independent unit test covering the empty-array cases and a populated case as a regression guard. Co-Authored-By: Claude Opus 4.8 --- Changelog.md | 3 + .../nextcloud/api/utils/JsonAnswerParser.java | 5 ++ .../api/utils/JsonAnswerParserTest.java | 89 +++++++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 src/test/java/org/aarboard/nextcloud/api/utils/JsonAnswerParserTest.java 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)); + } +}