From 7911c637dc6af8424a4530618e2c1d33d00ee9c3 Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Fri, 16 Jan 2026 21:46:27 +0100 Subject: [PATCH 01/17] Fix nested message object in API responses #12096 --- .../12096-fix-ok-message-nested-object.md | 15 +++++++++++++++ .../harvard/iq/dataverse/api/AbstractApiBean.java | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 doc/release-notes/12096-fix-ok-message-nested-object.md diff --git a/doc/release-notes/12096-fix-ok-message-nested-object.md b/doc/release-notes/12096-fix-ok-message-nested-object.md new file mode 100644 index 00000000000..84ad16c63ae --- /dev/null +++ b/doc/release-notes/12096-fix-ok-message-nested-object.md @@ -0,0 +1,15 @@ +### API Response Format Fix for `message` Field + +The `message` field in API responses from certain endpoints was incorrectly returned as a nested object (`{"message": {"message": "..."}}`) instead of a plain string (`{"message": "..."}`). + +This has been fixed. The following endpoints now return the `message` field as a string, consistent with all other API responses: + +- `POST /api/datasets/{id}/add` (when uploading duplicate files) +- `PUT /api/admin/settings` +- `PUT /api/dataverses/{id}` +- `PUT /api/dataverses/{id}/inputLevels` +- `POST /api/admin/savedsearches` +- `PUT /api/harvest/clients/{nickName}` +- `PUT /api/harvest/server/oaisets/{specname}` + +**Note:** If you implemented a workaround to handle the nested `message` object, you may need to update your code to expect a plain string instead. diff --git a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java index 2ee5730153e..abe9c01e053 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -997,7 +997,7 @@ protected Response ok( String msg ) { protected Response ok( String msg, JsonObjectBuilder bld ) { return Response.ok().entity(Json.createObjectBuilder() .add("status", ApiConstants.STATUS_OK) - .add("message", Json.createObjectBuilder().add("message",msg)) + .add("message", msg) .add("data", bld).build()) .type(MediaType.APPLICATION_JSON) .build(); From 2e43bd766f29f901af268c91a307a99d1a529568 Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Sat, 17 Jan 2026 01:13:45 +0100 Subject: [PATCH 02/17] Fix integration tests and add API changelog entry --- doc/sphinx-guides/source/api/changelog.rst | 5 +++++ src/test/java/edu/harvard/iq/dataverse/api/AdminIT.java | 2 +- .../java/edu/harvard/iq/dataverse/api/DataversesIT.java | 8 ++++---- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/doc/sphinx-guides/source/api/changelog.rst b/doc/sphinx-guides/source/api/changelog.rst index 4c7a5914b1e..5ca35670560 100644 --- a/doc/sphinx-guides/source/api/changelog.rst +++ b/doc/sphinx-guides/source/api/changelog.rst @@ -7,6 +7,11 @@ This API changelog is experimental and we would love feedback on its usefulness. :local: :depth: 1 +v6.10 +----- + +- Several API endpoints that return both a ``message`` and ``data`` field were incorrectly returning the message as a nested object (``{"message":{"message":"..."}}``). This has been fixed so that the message is now a plain string (``{"message":"..."}``). Affected endpoints: ``POST /api/datasets/{id}/add`` (duplicate file warning), ``PUT /api/admin/settings``, ``PUT /api/dataverses/{id}``, ``PUT /api/dataverses/{id}/inputLevels``, ``POST /api/admin/savedsearches``, ``PUT /api/harvest/clients/{nickName}``, ``PUT /api/harvest/server/oaisets/{specname}``. See :ref:`12096`. + v6.9 ---- diff --git a/src/test/java/edu/harvard/iq/dataverse/api/AdminIT.java b/src/test/java/edu/harvard/iq/dataverse/api/AdminIT.java index 0313c460816..68c910568cd 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/AdminIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/AdminIT.java @@ -114,7 +114,7 @@ void testSettingsRoundTrip() { .assertThat() .statusCode(OK.getStatusCode()) .body("status", equalTo("OK")) - .body("message.message", containsString("successfully updated")); + .body("message", containsString("successfully updated")); // Step 5: Verify the harmless setting is gone (restored to original state) Response verifyRestoredResponse = UtilIT.getSetting(harmlessSetting); diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java index dc658c7134b..a3181fda3d9 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java @@ -1120,7 +1120,7 @@ public void testAttributesApi() { Response changeAttributeResp = UtilIT.setCollectionAttribute(collectionAlias, "name", newCollectionName, apiToken); changeAttributeResp.then().assertThat() .statusCode(OK.getStatusCode()) - .body("message.message", equalTo("Update successful")); + .body("message", equalTo("Update successful")); // Change the description of the collection: @@ -1128,7 +1128,7 @@ public void testAttributesApi() { changeAttributeResp = UtilIT.setCollectionAttribute(collectionAlias, "description", newDescription, apiToken); changeAttributeResp.then().assertThat() .statusCode(OK.getStatusCode()) - .body("message.message", equalTo("Update successful")); + .body("message", equalTo("Update successful")); // Change the affiliation of the collection: @@ -1136,7 +1136,7 @@ public void testAttributesApi() { changeAttributeResp = UtilIT.setCollectionAttribute(collectionAlias, "affiliation", newAffiliation, apiToken); changeAttributeResp.then().assertThat() .statusCode(OK.getStatusCode()) - .body("message.message", equalTo("Update successful")); + .body("message", equalTo("Update successful")); // Cannot update filePIDsEnabled from a regular user: @@ -1149,7 +1149,7 @@ public void testAttributesApi() { changeAttributeResp = UtilIT.setCollectionAttribute(collectionAlias, "alias", newCollectionAlias, apiToken); changeAttributeResp.then().assertThat() .statusCode(OK.getStatusCode()) - .body("message.message", equalTo("Update successful")); + .body("message", equalTo("Update successful")); // Check on the collection, under the new alias: From 0a9ce327e314f8908b4661ac7799c13a9dd18fb8 Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Sat, 17 Jan 2026 01:17:40 +0100 Subject: [PATCH 03/17] Fix RST reference syntax in changelog --- doc/sphinx-guides/source/api/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/api/changelog.rst b/doc/sphinx-guides/source/api/changelog.rst index 5ca35670560..4cd6f10dacc 100644 --- a/doc/sphinx-guides/source/api/changelog.rst +++ b/doc/sphinx-guides/source/api/changelog.rst @@ -10,7 +10,7 @@ This API changelog is experimental and we would love feedback on its usefulness. v6.10 ----- -- Several API endpoints that return both a ``message`` and ``data`` field were incorrectly returning the message as a nested object (``{"message":{"message":"..."}}``). This has been fixed so that the message is now a plain string (``{"message":"..."}``). Affected endpoints: ``POST /api/datasets/{id}/add`` (duplicate file warning), ``PUT /api/admin/settings``, ``PUT /api/dataverses/{id}``, ``PUT /api/dataverses/{id}/inputLevels``, ``POST /api/admin/savedsearches``, ``PUT /api/harvest/clients/{nickName}``, ``PUT /api/harvest/server/oaisets/{specname}``. See :ref:`12096`. +- Several API endpoints that return both a ``message`` and ``data`` field were incorrectly returning the message as a nested object (``{"message":{"message":"..."}}``). This has been fixed so that the message is now a plain string (``{"message":"..."}``). Affected endpoints: ``POST /api/datasets/{id}/add`` (duplicate file warning), ``PUT /api/admin/settings``, ``PUT /api/dataverses/{id}``, ``PUT /api/dataverses/{id}/inputLevels``, ``POST /api/admin/savedsearches``, ``PUT /api/harvest/clients/{nickName}``, ``PUT /api/harvest/server/oaisets/{specname}``. See `#12096 `_. v6.9 ---- From bd27aafd143b723fed0894a08ada1fa433b76d86 Mon Sep 17 00:00:00 2001 From: Eryk Kulikowski Date: Mon, 9 Feb 2026 10:37:19 +0100 Subject: [PATCH 04/17] Add feature flag for legacy API message format Adds API_MESSAGE_FIELD_LEGACY feature flag that allows admins to revert to the old (buggy) nested message format if they have integrations that depend on it. The fix is now the default behavior. Set dataverse.feature.api-message-field-legacy=true to use the legacy format. This flag will be removed in a future version. --- .../12096-fix-ok-message-nested-object.md | 8 +++++- doc/sphinx-guides/source/api/changelog.rst | 2 +- .../iq/dataverse/api/AbstractApiBean.java | 16 ++++++++--- .../iq/dataverse/settings/FeatureFlags.java | 27 +++++++++++++++++++ 4 files changed, 47 insertions(+), 6 deletions(-) diff --git a/doc/release-notes/12096-fix-ok-message-nested-object.md b/doc/release-notes/12096-fix-ok-message-nested-object.md index 84ad16c63ae..75f8cf4fe6d 100644 --- a/doc/release-notes/12096-fix-ok-message-nested-object.md +++ b/doc/release-notes/12096-fix-ok-message-nested-object.md @@ -12,4 +12,10 @@ This has been fixed. The following endpoints now return the `message` field as a - `PUT /api/harvest/clients/{nickName}` - `PUT /api/harvest/server/oaisets/{specname}` -**Note:** If you implemented a workaround to handle the nested `message` object, you may need to update your code to expect a plain string instead. +**Note:** If you have integrations that implemented workarounds for the nested `message` object, you may need to update your code to expect a plain string instead. If you need time to update your integrations, you can temporarily revert to the legacy behavior by setting the feature flag: + +``` +dataverse.feature.api-message-field-legacy=true +``` + +This flag will be removed in a future version. diff --git a/doc/sphinx-guides/source/api/changelog.rst b/doc/sphinx-guides/source/api/changelog.rst index 4cd6f10dacc..5c339dea925 100644 --- a/doc/sphinx-guides/source/api/changelog.rst +++ b/doc/sphinx-guides/source/api/changelog.rst @@ -10,7 +10,7 @@ This API changelog is experimental and we would love feedback on its usefulness. v6.10 ----- -- Several API endpoints that return both a ``message`` and ``data`` field were incorrectly returning the message as a nested object (``{"message":{"message":"..."}}``). This has been fixed so that the message is now a plain string (``{"message":"..."}``). Affected endpoints: ``POST /api/datasets/{id}/add`` (duplicate file warning), ``PUT /api/admin/settings``, ``PUT /api/dataverses/{id}``, ``PUT /api/dataverses/{id}/inputLevels``, ``POST /api/admin/savedsearches``, ``PUT /api/harvest/clients/{nickName}``, ``PUT /api/harvest/server/oaisets/{specname}``. See `#12096 `_. +- Several API endpoints that return both a ``message`` and ``data`` field were incorrectly returning the message as a nested object (``{"message":{"message":"..."}}``). This has been fixed so that the message is now a plain string (``{"message":"..."}``). If you have integrations that depend on the old behavior, you can temporarily revert by setting ``dataverse.feature.api-message-field-legacy=true``. This flag will be removed in a future version. Affected endpoints: ``POST /api/datasets/{id}/add`` (duplicate file warning), ``PUT /api/admin/settings``, ``PUT /api/dataverses/{id}``, ``PUT /api/dataverses/{id}/inputLevels``, ``POST /api/admin/savedsearches``, ``PUT /api/harvest/clients/{nickName}``, ``PUT /api/harvest/server/oaisets/{specname}``. See `#12096 `_. v6.9 ---- diff --git a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java index abe9c01e053..d6ead733c94 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -995,10 +995,18 @@ protected Response ok( String msg ) { } protected Response ok( String msg, JsonObjectBuilder bld ) { - return Response.ok().entity(Json.createObjectBuilder() - .add("status", ApiConstants.STATUS_OK) - .add("message", msg) - .add("data", bld).build()) + JsonObjectBuilder response = Json.createObjectBuilder() + .add("status", ApiConstants.STATUS_OK); + + // Legacy mode returns message as nested object for backward compatibility + // with integrations that worked around the bug. This will be removed in a future version. + if (FeatureFlags.API_MESSAGE_FIELD_LEGACY.enabled()) { + response.add("message", Json.createObjectBuilder().add("message", msg)); + } else { + response.add("message", msg); + } + + return Response.ok().entity(response.add("data", bld).build()) .type(MediaType.APPLICATION_JSON) .build(); } diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java b/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java index 2e86fae610e..89b6ede0e9b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java @@ -250,6 +250,33 @@ public enum FeatureFlags { */ ONLY_UPDATE_DATACITE_WHEN_NEEDED("only-update-datacite-when-needed"), + /** + * Reverts to the legacy (buggy) API response format for the {@code ok(String msg, JsonObjectBuilder bld)} + * method in AbstractApiBean. When enabled, the {@code message} field is returned as a nested object + * ({@code {"message":{"message":"..."}}}) instead of the corrected plain string format + * ({@code {"message":"..."}}). + * + *

This flag is provided as a temporary workaround for integrations that may have implemented + * workarounds for the buggy behavior. The following endpoints are affected:

+ *
    + *
  • POST /api/datasets/{id}/add (duplicate file warning)
  • + *
  • PUT /api/admin/settings
  • + *
  • PUT /api/dataverses/{id}
  • + *
  • PUT /api/dataverses/{id}/inputLevels
  • + *
  • POST /api/admin/savedsearches
  • + *
  • PUT /api/harvest/clients/{nickName}
  • + *
  • PUT /api/harvest/server/oaisets/{specname}
  • + *
+ * + *

This flag will be removed in a future version. Please update your integrations to expect + * the corrected message format.

+ * + * @apiNote Raise flag by setting "dataverse.feature.api-message-field-legacy" + * @since Dataverse 6.10 + * @see Issue #12096 + */ + API_MESSAGE_FIELD_LEGACY("api-message-field-legacy"), + ; final String flag; From 18f42aaa152e247e40270bebd0d4f608f002bac1 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 11 Feb 2026 01:42:16 +0100 Subject: [PATCH 05/17] refactor(api): remove redundant `this.` qualifier in `BatchImport.java` Replaced `this.error(...)` with `error(...)` for consistency with recent style updates in error handling. --- src/main/java/edu/harvard/iq/dataverse/api/BatchImport.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/BatchImport.java b/src/main/java/edu/harvard/iq/dataverse/api/BatchImport.java index a2d06bff93e..7bdeceba13b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/BatchImport.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/BatchImport.java @@ -89,7 +89,7 @@ public Response postImport(@Context ContainerRequestContext crc, String body, @Q JsonObjectBuilder status = importService.doImport(dataverseRequest, owner, body, filename, ImportType.NEW, cleanupLog); return this.ok(status); } catch (ImportException | IOException e) { - return this.error(Response.Status.BAD_REQUEST, e.getMessage()); + return error(Response.Status.BAD_REQUEST, e.getMessage()); } } @@ -134,7 +134,7 @@ private Response startBatchJob(User user, String fileDir, String parentIdtf, Str batchService.processFilePath(fileDir, parentIdtf, dataverseRequest, owner, importType, createDV); } catch (ImportException e) { - return this.error(Response.Status.BAD_REQUEST, "Import Exception, " + e.getMessage()); + return error(Response.Status.BAD_REQUEST, "Import Exception, " + e.getMessage()); } return this.accepted(); } From aa1a295fea8beb7c928d6240238c6804c68d50c1 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 11 Feb 2026 01:56:50 +0100 Subject: [PATCH 06/17] refactor(api): move constants from `AbstractApiBean` to `ApiConstants` and remove duplicates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consolidated shared API constants—such as header names, status codes, and response messages—from `AbstractApiBean` into the dedicated `ApiConstants` class. Replaced direct usages with references to `ApiConstants` for improved maintainability and consistency. --- .../iq/dataverse/api/AbstractApiBean.java | 54 +++++++++---------- .../iq/dataverse/api/ApiConstants.java | 11 ++++ .../engine/command/DataverseRequest.java | 4 +- .../harvard/iq/dataverse/api/DatasetsIT.java | 2 +- .../harvard/iq/dataverse/api/SearchIT.java | 2 +- 5 files changed, 39 insertions(+), 34 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java index d6ead733c94..96d3aff2eb3 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -76,12 +76,6 @@ public abstract class AbstractApiBean { private static final Logger logger = Logger.getLogger(AbstractApiBean.class.getName()); - private static final String DATAVERSE_KEY_HEADER_NAME = "X-Dataverse-key"; - private static final String PERSISTENT_ID_KEY=":persistentId"; - private static final String ALIAS_KEY=":alias"; - public static final String STATUS_WF_IN_PROGRESS = "WORKFLOW_IN_PROGRESS"; - public static final String DATAVERSE_WORKFLOW_INVOCATION_HEADER_NAME = "X-Dataverse-invocationID"; - public static final String RESPONSE_MESSAGE_AUTHENTICATED_USER_REQUIRED = "Only authenticated users can perform the requested operation"; /** * Utility class to convey a proper error response using Java's exceptions. @@ -307,7 +301,7 @@ protected String getRequestParameter( String key ) { } protected String getRequestApiKey() { - String headerParamApiKey = httpRequest.getHeader(DATAVERSE_KEY_HEADER_NAME); + String headerParamApiKey = httpRequest.getHeader(ApiConstants.DATAVERSE_KEY_HEADER_NAME); String queryParamApiKey = httpRequest.getParameter("key"); return headerParamApiKey!=null ? headerParamApiKey : queryParamApiKey; @@ -422,11 +416,11 @@ protected Dataset findDatasetOrDie(String id, boolean deep) throws WrappedRespon } } else { String persistentId = id; - if (id.equals(PERSISTENT_ID_KEY)) { - persistentId = getRequestParameter(PERSISTENT_ID_KEY.substring(1)); + if (id.equals(ApiConstants.PERSISTENT_ID_KEY)) { + persistentId = getRequestParameter(ApiConstants.PERSISTENT_ID_KEY.substring(1)); if (persistentId == null) { throw new WrappedResponse( - badRequest(BundleUtil.getStringFromBundle("find.dataset.error.dataset_id_is_null", Collections.singletonList(PERSISTENT_ID_KEY.substring(1))))); + badRequest(BundleUtil.getStringFromBundle("find.dataset.error.dataset_id_is_null", Collections.singletonList(ApiConstants.PERSISTENT_ID_KEY.substring(1))))); } } GlobalId globalId; @@ -447,7 +441,7 @@ protected Dataset findDatasetOrDie(String id, boolean deep) throws WrappedRespon fprLogService.logEntry(entry); } throw new WrappedResponse( - notFound(BundleUtil.getStringFromBundle("find.dataset.error.dataset_id_is_null", Collections.singletonList(PERSISTENT_ID_KEY.substring(1))))); + notFound(BundleUtil.getStringFromBundle("find.dataset.error.dataset_id_is_null", Collections.singletonList(ApiConstants.PERSISTENT_ID_KEY.substring(1))))); } } if (deep) { @@ -509,11 +503,11 @@ protected void validateInternalTimestampIsNotOutdated(DvObject dvObject, String protected DataFile findDataFileOrDie(String id) throws WrappedResponse { DataFile datafile; - if (id.equals(PERSISTENT_ID_KEY)) { - String persistentId = getRequestParameter(PERSISTENT_ID_KEY.substring(1)); + if (id.equals(ApiConstants.PERSISTENT_ID_KEY)) { + String persistentId = getRequestParameter(ApiConstants.PERSISTENT_ID_KEY.substring(1)); if (persistentId == null) { throw new WrappedResponse( - badRequest(BundleUtil.getStringFromBundle("find.dataset.error.dataset_id_is_null", Collections.singletonList(PERSISTENT_ID_KEY.substring(1))))); + badRequest(BundleUtil.getStringFromBundle("find.dataset.error.dataset_id_is_null", Collections.singletonList(ApiConstants.PERSISTENT_ID_KEY.substring(1))))); } datafile = fileService.findByGlobalId(persistentId); if (datafile == null) { @@ -541,8 +535,8 @@ protected DataFile findDataFileOrDie(String id) throws WrappedResponse { protected DataverseRole findRoleOrDie(String id) throws WrappedResponse { DataverseRole role; - if (id.equals(ALIAS_KEY)) { - String alias = getRequestParameter(ALIAS_KEY.substring(1)); + if (id.equals(ApiConstants.ALIAS_KEY)) { + String alias = getRequestParameter(ApiConstants.ALIAS_KEY.substring(1)); try { return em.createNamedQuery("DataverseRole.findDataverseRoleByAlias", DataverseRole.class) .setParameter("alias", alias) @@ -574,11 +568,11 @@ protected DatasetLinkingDataverse findDatasetLinkingDataverseOrDie(String datase DatasetLinkingDataverse dsld; Dataverse linkingDataverse = findDataverseOrDie(linkingDataverseId); - if (datasetId.equals(PERSISTENT_ID_KEY)) { - String persistentId = getRequestParameter(PERSISTENT_ID_KEY.substring(1)); + if (datasetId.equals(ApiConstants.PERSISTENT_ID_KEY)) { + String persistentId = getRequestParameter(ApiConstants.PERSISTENT_ID_KEY.substring(1)); if (persistentId == null) { throw new WrappedResponse( - badRequest(BundleUtil.getStringFromBundle("find.dataset.error.dataset_id_is_null", Collections.singletonList(PERSISTENT_ID_KEY.substring(1))))); + badRequest(BundleUtil.getStringFromBundle("find.dataset.error.dataset_id_is_null", Collections.singletonList(ApiConstants.PERSISTENT_ID_KEY.substring(1))))); } Dataset dataset = datasetSvc.findByGlobalId(persistentId); @@ -1045,8 +1039,8 @@ protected Response ok(InputStream inputStream) { protected Response created( String uri, JsonObjectBuilder bld ) { return Response.created( URI.create(uri) ) .entity( Json.createObjectBuilder() - .add("status", "OK") - .add("data", bld).build()) + .add(ApiConstants.STATUS_FIELD, ApiConstants.STATUS_OK) + .add(ApiConstants.DATA_FIELD, bld).build()) .type(MediaType.APPLICATION_JSON) .build(); } @@ -1054,15 +1048,15 @@ protected Response created( String uri, JsonObjectBuilder bld ) { protected Response accepted(JsonObjectBuilder bld) { return Response.accepted() .entity(Json.createObjectBuilder() - .add("status", STATUS_WF_IN_PROGRESS) - .add("data",bld).build() + .add(ApiConstants.STATUS_FIELD, ApiConstants.STATUS_WF_IN_PROGRESS) + .add(ApiConstants.DATA_FIELD, bld).build() ).build(); } protected Response accepted() { return Response.accepted() .entity(Json.createObjectBuilder() - .add("status", STATUS_WF_IN_PROGRESS).build() + .add(ApiConstants.STATUS_FIELD, ApiConstants.STATUS_WF_IN_PROGRESS).build() ).build(); } @@ -1077,8 +1071,8 @@ protected Response badRequest( String msg ) { protected Response badRequest(String msg, Map fieldErrors) { return Response.status(Status.BAD_REQUEST) .entity(NullSafeJsonBuilder.jsonObjectBuilder() - .add("status", ApiConstants.STATUS_ERROR) - .add("message", msg) + .add(ApiConstants.STATUS_FIELD, ApiConstants.STATUS_ERROR) + .add(ApiConstants.MESSAGE_FIELD, msg) .add("fieldErrors", Json.createObjectBuilder(fieldErrors).build()) .build() ) @@ -1111,7 +1105,7 @@ protected Response conflict( String msg ) { } protected Response authenticatedUserRequired() { - return error(Status.UNAUTHORIZED, RESPONSE_MESSAGE_AUTHENTICATED_USER_REQUIRED); + return error(Status.UNAUTHORIZED, ApiConstants.RESPONSE_MESSAGE_AUTHENTICATED_USER_REQUIRED); } protected Response permissionError( PermissionException pe ) { @@ -1136,9 +1130,9 @@ protected Response unauthorized( String message ) { protected static Response error( Status sts, String msg ) { return Response.status(sts) - .entity( NullSafeJsonBuilder.jsonObjectBuilder() - .add("status", ApiConstants.STATUS_ERROR) - .add( "message", msg ).build() + .entity(NullSafeJsonBuilder.jsonObjectBuilder() + .add(ApiConstants.STATUS_FIELD, ApiConstants.STATUS_ERROR) + .add(ApiConstants.MESSAGE_FIELD, msg ).build() ).type(MediaType.APPLICATION_JSON_TYPE).build(); } } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/ApiConstants.java b/src/main/java/edu/harvard/iq/dataverse/api/ApiConstants.java index 15114085c21..5e3d5a696e9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/ApiConstants.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/ApiConstants.java @@ -7,13 +7,24 @@ private ApiConstants() { } // Statuses + public static final String STATUS_FIELD = "status"; public static final String STATUS_OK = "OK"; public static final String STATUS_ERROR = "ERROR"; + public static final String STATUS_WF_IN_PROGRESS = "WORKFLOW_IN_PROGRESS"; + + public static final String DATA_FIELD = "data"; + public static final String TOTAL_COUNT_FIELD = "totalCount"; + public static final String MESSAGE_FIELD = "message "; // Authentication public static final String CONTAINER_REQUEST_CONTEXT_USER = "user"; + public static final String ALIAS_KEY=":alias"; + public static final String DATAVERSE_KEY_HEADER_NAME = "X-Dataverse-key"; + public static final String DATAVERSE_WORKFLOW_INVOCATION_HEADER_NAME = "X-Dataverse-invocationID"; + public static final String RESPONSE_MESSAGE_AUTHENTICATED_USER_REQUIRED = "Only authenticated users can perform the requested operation"; // Dataset + public static final String PERSISTENT_ID_KEY=":persistentId"; public static final String DS_VERSION_LATEST = ":latest"; public static final String DS_VERSION_DRAFT = ":draft"; public static final String DS_VERSION_LATEST_PUBLISHED = ":latest-published"; diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/DataverseRequest.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/DataverseRequest.java index 4d3ec2842a1..5094e84cb75 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/DataverseRequest.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/DataverseRequest.java @@ -1,6 +1,6 @@ package edu.harvard.iq.dataverse.engine.command; -import edu.harvard.iq.dataverse.api.AbstractApiBean; +import edu.harvard.iq.dataverse.api.ApiConstants; import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IpAddress; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.User; @@ -139,7 +139,7 @@ public DataverseRequest(User aUser, HttpServletRequest aHttpServletRequest) { } } - String headerParamWFKey = aHttpServletRequest.getHeader(AbstractApiBean.DATAVERSE_WORKFLOW_INVOCATION_HEADER_NAME); + String headerParamWFKey = aHttpServletRequest.getHeader(ApiConstants.DATAVERSE_WORKFLOW_INVOCATION_HEADER_NAME); String queryParamWFKey = aHttpServletRequest.getParameter("invocationId"); invocationId = headerParamWFKey!=null ? headerParamWFKey : queryParamWFKey; diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java index b7cbb37480c..9f88d9311d7 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java @@ -2599,7 +2599,7 @@ public void testCreateDatasetWithDcmDependency() { getRsyncScriptPermErrorGuest.then().assertThat() .statusCode(UNAUTHORIZED.getStatusCode()) .contentType(ContentType.JSON) - .body("message", equalTo(AbstractApiBean.RESPONSE_MESSAGE_AUTHENTICATED_USER_REQUIRED)); + .body("message", equalTo(ApiConstants.RESPONSE_MESSAGE_AUTHENTICATED_USER_REQUIRED)); Response createNoPermsUser = UtilIT.createRandomUser(); String noPermsUsername = UtilIT.getUsernameFromResponse(createNoPermsUser); diff --git a/src/test/java/edu/harvard/iq/dataverse/api/SearchIT.java b/src/test/java/edu/harvard/iq/dataverse/api/SearchIT.java index ca9e19e1bbf..168b74ae7e2 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/SearchIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/SearchIT.java @@ -163,7 +163,7 @@ public void testSearchPermisions() { Response dataverse47behaviorOfTokensBeingRequired = UtilIT.search("id:dataset_" + datasetId, nullToken); dataverse47behaviorOfTokensBeingRequired.prettyPrint(); dataverse47behaviorOfTokensBeingRequired.then().assertThat() - .body("message", CoreMatchers.equalTo(AbstractApiBean.RESPONSE_MESSAGE_AUTHENTICATED_USER_REQUIRED)) + .body("message", CoreMatchers.equalTo(ApiConstants.RESPONSE_MESSAGE_AUTHENTICATED_USER_REQUIRED)) .statusCode(UNAUTHORIZED.getStatusCode()); Response reEnableTokenlessSearch = UtilIT.deleteSetting(SettingsServiceBean.Key.SearchApiRequiresToken); From e13bd3454fe3abaec5562ab3a3a97952a4f95daf Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 11 Feb 2026 01:59:52 +0100 Subject: [PATCH 07/17] refactor(api): consolidate `ok()` response methods and simplify return structure Unified the multiple `ok()` overloads into a single primary method accepting `JsonValue`s for `data and` `message` fields, plus `totalCount` Long field, reducing duplication and ensuring consistent JSON response formatting across API endpoints. --- .../iq/dataverse/api/AbstractApiBean.java | 58 ++++++++----------- 1 file changed, 24 insertions(+), 34 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java index 96d3aff2eb3..796b1c379ba 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -942,42 +942,36 @@ private Response handleDataverseRequestHandlerException(Exception ex) { * HTTP Response methods * \* ====================== */ - protected Response ok( JsonArrayBuilder bld ) { - return Response.ok(Json.createObjectBuilder() - .add("status", ApiConstants.STATUS_OK) - .add("data", bld).build()) - .type(MediaType.APPLICATION_JSON).build(); + protected Response ok(JsonValue value, JsonValue message, Long totalCount) { + return Response.status(Response.Status.OK) + .entity(NullSafeJsonBuilder.jsonObjectBuilder() + .add(ApiConstants.STATUS_FIELD, ApiConstants.STATUS_OK) + .add(ApiConstants.MESSAGE_FIELD, message) + .add(ApiConstants.TOTAL_COUNT_FIELD, totalCount) + .add(ApiConstants.DATA_FIELD, value)) + .type(MediaType.APPLICATION_JSON) + .build(); + + } + + protected Response ok(JsonArrayBuilder bld) { + return ok(bld.build(), null, null); } - protected Response ok( JsonArrayBuilder bld , long totalCount) { - return Response.ok(Json.createObjectBuilder() - .add("status", ApiConstants.STATUS_OK) - .add("totalCount", totalCount) - .add("data", bld).build()) - .type(MediaType.APPLICATION_JSON).build(); + protected Response ok(JsonArrayBuilder bld, long totalCount) { + return ok(bld.build(), null, totalCount); } - protected Response ok( JsonArray ja ) { - return Response.ok(Json.createObjectBuilder() - .add("status", ApiConstants.STATUS_OK) - .add("data", ja).build()) - .type(MediaType.APPLICATION_JSON).build(); + protected Response ok(JsonArray ja) { + return ok(ja, null, null); } - protected Response ok( JsonObjectBuilder bld ) { - return Response.ok( Json.createObjectBuilder() - .add("status", ApiConstants.STATUS_OK) - .add("data", bld).build() ) - .type(MediaType.APPLICATION_JSON) - .build(); + protected Response ok(JsonObjectBuilder bld) { + return ok(bld.build(), null, null); } - protected Response ok( JsonObject jo ) { - return Response.ok( Json.createObjectBuilder() - .add("status", ApiConstants.STATUS_OK) - .add("data", jo).build() ) - .type(MediaType.APPLICATION_JSON) - .build(); + protected Response ok(JsonObject jo) { + return ok(jo, null, null); } protected Response ok( String msg ) { @@ -1006,15 +1000,11 @@ protected Response ok( String msg, JsonObjectBuilder bld ) { } protected Response ok( boolean value ) { - return Response.ok().entity(Json.createObjectBuilder() - .add("status", ApiConstants.STATUS_OK) - .add("data", value).build() ).build(); + return ok(value ? JsonValue.TRUE : JsonValue.FALSE, null, null); } protected Response ok(long value) { - return Response.ok().entity(Json.createObjectBuilder() - .add("status", ApiConstants.STATUS_OK) - .add("data", value).build()).build(); + return ok(Json.createValue(value), null, null); } /** From 15acb22d9522f5c9df7e9f499092feb87c144371 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 11 Feb 2026 02:18:35 +0100 Subject: [PATCH 08/17] refactor(api): add legacy message field support for ok(String) response creation Use feature flag `API_MESSAGE_FIELD_LEGACY` to preserve the old `{data:{message:"..."}}` response format if needed in an installation. --- .../iq/dataverse/api/AbstractApiBean.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java index 796b1c379ba..ad3de83db03 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -974,12 +974,17 @@ protected Response ok(JsonObject jo) { return ok(jo, null, null); } - protected Response ok( String msg ) { - return Response.ok().entity(Json.createObjectBuilder() - .add("status", ApiConstants.STATUS_OK) - .add("data", Json.createObjectBuilder().add("message",msg)).build() ) - .type(MediaType.APPLICATION_JSON) - .build(); + protected Response ok(String msg) { + // An instance may opt back into using the old {data:{message:"$msg"}} way. + // TODO: This will be removed in a future version. + if (FeatureFlags.API_MESSAGE_FIELD_LEGACY.enabled()) { + return ok(Json.createObjectBuilder() + .add("message", msg) + .build(), + null, null); + } else { + return ok(null, Json.createValue(msg), null); + } } protected Response ok( String msg, JsonObjectBuilder bld ) { From f89293bdef21dd83a13e148a3bcef35adf1ff293 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 11 Feb 2026 02:24:48 +0100 Subject: [PATCH 09/17] refactor(api): simplify `ok(String, JsonObjectBuilder)` response creation Consolidated `ok()` logic by delegating to the unified `ok(JsonValue, JsonValue, Long)` method, removing redundant response construction. Keeping the support to create the wrapped legacy {message:{message:value}} style. --- .../iq/dataverse/api/AbstractApiBean.java | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java index ad3de83db03..7730ec8f7e1 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -987,21 +987,15 @@ protected Response ok(String msg) { } } - protected Response ok( String msg, JsonObjectBuilder bld ) { - JsonObjectBuilder response = Json.createObjectBuilder() - .add("status", ApiConstants.STATUS_OK); - + protected Response ok(String msg, JsonObjectBuilder bld) { // Legacy mode returns message as nested object for backward compatibility - // with integrations that worked around the bug. This will be removed in a future version. + // with integrations that worked around the bug. + // TODO: This will be removed in a future version. if (FeatureFlags.API_MESSAGE_FIELD_LEGACY.enabled()) { - response.add("message", Json.createObjectBuilder().add("message", msg)); + return ok(bld.build(), Json.createObjectBuilder().add(ApiConstants.MESSAGE_FIELD, msg).build(), null); } else { - response.add("message", msg); + return ok(bld.build(), Json.createValue(msg), null); } - - return Response.ok().entity(response.add("data", bld).build()) - .type(MediaType.APPLICATION_JSON) - .build(); } protected Response ok( boolean value ) { From 1f83f659c238bc437ff631e95ff08b1b1bb09467 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 11 Feb 2026 03:12:02 +0100 Subject: [PATCH 10/17] refactor(settings): migrate `API_MESSAGE_FIELD_LEGACY` flag to `JvmSettings` Moved the legacy API message response style toggle from `FeatureFlags` to `JvmSettings`, using `LEGACY_API_RESPONSE_MESSAGE_STYLE`. Updated `AbstractApiBean` to use the new setting, and removed the deprecated `API_MESSAGE_FIELD_LEGACY` entry from `FeatureFlags`. --- .../iq/dataverse/api/AbstractApiBean.java | 6 ++-- .../iq/dataverse/settings/FeatureFlags.java | 31 +------------------ .../iq/dataverse/settings/JvmSettings.java | 4 +++ 3 files changed, 9 insertions(+), 32 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java index 7730ec8f7e1..19268dbf26c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -32,6 +32,7 @@ import edu.harvard.iq.dataverse.metrics.MetricsServiceBean; import edu.harvard.iq.dataverse.search.savedsearch.SavedSearchServiceBean; import edu.harvard.iq.dataverse.settings.FeatureFlags; +import edu.harvard.iq.dataverse.settings.JvmSettings; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.DateUtil; @@ -976,8 +977,9 @@ protected Response ok(JsonObject jo) { protected Response ok(String msg) { // An instance may opt back into using the old {data:{message:"$msg"}} way. + // This is a highly used response builder! // TODO: This will be removed in a future version. - if (FeatureFlags.API_MESSAGE_FIELD_LEGACY.enabled()) { + if (JvmSettings.LEGACY_API_RESPONSE_MESSAGE_STYLE.lookupOptional(Boolean.class).orElse(false)) { return ok(Json.createObjectBuilder() .add("message", msg) .build(), @@ -991,7 +993,7 @@ protected Response ok(String msg, JsonObjectBuilder bld) { // Legacy mode returns message as nested object for backward compatibility // with integrations that worked around the bug. // TODO: This will be removed in a future version. - if (FeatureFlags.API_MESSAGE_FIELD_LEGACY.enabled()) { + if (JvmSettings.LEGACY_API_RESPONSE_MESSAGE_STYLE.lookupOptional(Boolean.class).orElse(false)) { return ok(bld.build(), Json.createObjectBuilder().add(ApiConstants.MESSAGE_FIELD, msg).build(), null); } else { return ok(bld.build(), Json.createValue(msg), null); diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java b/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java index 89b6ede0e9b..8d24dd2aa7f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java @@ -248,36 +248,7 @@ public enum FeatureFlags { * "dataverse.feature.only-update-datacite-when-needed" * @since Dataverse 6.9 */ - ONLY_UPDATE_DATACITE_WHEN_NEEDED("only-update-datacite-when-needed"), - - /** - * Reverts to the legacy (buggy) API response format for the {@code ok(String msg, JsonObjectBuilder bld)} - * method in AbstractApiBean. When enabled, the {@code message} field is returned as a nested object - * ({@code {"message":{"message":"..."}}}) instead of the corrected plain string format - * ({@code {"message":"..."}}). - * - *

This flag is provided as a temporary workaround for integrations that may have implemented - * workarounds for the buggy behavior. The following endpoints are affected:

- *
    - *
  • POST /api/datasets/{id}/add (duplicate file warning)
  • - *
  • PUT /api/admin/settings
  • - *
  • PUT /api/dataverses/{id}
  • - *
  • PUT /api/dataverses/{id}/inputLevels
  • - *
  • POST /api/admin/savedsearches
  • - *
  • PUT /api/harvest/clients/{nickName}
  • - *
  • PUT /api/harvest/server/oaisets/{specname}
  • - *
- * - *

This flag will be removed in a future version. Please update your integrations to expect - * the corrected message format.

- * - * @apiNote Raise flag by setting "dataverse.feature.api-message-field-legacy" - * @since Dataverse 6.10 - * @see Issue #12096 - */ - API_MESSAGE_FIELD_LEGACY("api-message-field-legacy"), - - ; + ONLY_UPDATE_DATACITE_WHEN_NEEDED("only-update-datacite-when-needed"); final String flag; final boolean defaultStatus; diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java b/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java index 05390ba8a8c..e1456df3856 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java @@ -109,6 +109,10 @@ public enum JvmSettings { // Avoids adding flag entries twice. FEATURE_FLAG(SCOPE_FLAGS), + // LEGACY BEHAVIOUR SETTINGS + SCOPE_LEGACY(PREFIX, "legacy"), + LEGACY_API_RESPONSE_MESSAGE_STYLE(SCOPE_LEGACY, "api-response-message-style"), + // METADATA SETTINGS SCOPE_METADATA(PREFIX, "metadata"), MDB_SYSTEM_METADATA_KEYS(SCOPE_METADATA, "block-system-metadata-keys"), From f475099144573fbe355a3c683d56df6852eb8168 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 11 Feb 2026 19:30:56 +0100 Subject: [PATCH 11/17] refactor(api): unify all API response message styles with feature flag The most often used style of embedding the message into the data field is not standardized, but only opt-in. As this is a breaking change, we soften the blow by allowing integrations, libraries, clients, and anyone else to test thoroughly before the alignment is made permanent at a later point. --- .../iq/dataverse/api/AbstractApiBean.java | 17 +++++++---------- .../iq/dataverse/settings/FeatureFlags.java | 18 +++++++++++++++++- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java index 19268dbf26c..5750a646336 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -976,22 +976,19 @@ protected Response ok(JsonObject jo) { } protected Response ok(String msg) { - // An instance may opt back into using the old {data:{message:"$msg"}} way. - // This is a highly used response builder! + // An instance may opt out of using the old {data:{message:"$msg"}} way. + // This is a highly used response builder, which is why this is an experimental opt-in change! // TODO: This will be removed in a future version. - if (JvmSettings.LEGACY_API_RESPONSE_MESSAGE_STYLE.lookupOptional(Boolean.class).orElse(false)) { - return ok(Json.createObjectBuilder() - .add("message", msg) - .build(), - null, null); - } else { + if (FeatureFlags.UNIFY_API_RESPONSE_MESSAGE_STYLE.enabled()) { return ok(null, Json.createValue(msg), null); + } else { + return ok(Json.createObjectBuilder().add("message", msg).build(), null, null); } } protected Response ok(String msg, JsonObjectBuilder bld) { - // Legacy mode returns message as nested object for backward compatibility - // with integrations that worked around the bug. + // Legacy mode returns message as nested object for backward compatibility with integrations that worked around the bug. + // This is a scarcely used way to build a response, mostly relevant to admins, which is why we make it opt-out. // TODO: This will be removed in a future version. if (JvmSettings.LEGACY_API_RESPONSE_MESSAGE_STYLE.lookupOptional(Boolean.class).orElse(false)) { return ok(bld.build(), Json.createObjectBuilder().add(ApiConstants.MESSAGE_FIELD, msg).build(), null); diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java b/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java index 8d24dd2aa7f..78871ff8459 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java @@ -248,7 +248,23 @@ public enum FeatureFlags { * "dataverse.feature.only-update-datacite-when-needed" * @since Dataverse 6.9 */ - ONLY_UPDATE_DATACITE_WHEN_NEEDED("only-update-datacite-when-needed"); + ONLY_UPDATE_DATACITE_WHEN_NEEDED("only-update-datacite-when-needed"), + + /** + * Historically, success messages have returned success messages as {data:{message:...}}. + * Error messages have been return as either {message:...} or {message:{message:...}}. + * While the alignment of error messages is opt-out (see {@link JvmSettings#LEGACY_API_RESPONSE_MESSAGE_STYLE}, + * changing the response format of ~230 success responses may cause a lot of friction. + * This feature flag makes sure any early adopters can change to the unified response layout, + * and it will graduate to the new default later. + * + * @apiNote Raise flag by setting "dataverse.feature.unify-api-response-message-style" + * @since Dataverse 6.10 + */ + UNIFY_API_RESPONSE_MESSAGE_STYLE("unify-api-response-message-style") + + ; + final String flag; final boolean defaultStatus; From 6403498d3169d7de020fd9021dcd090219571b93 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 11 Feb 2026 19:31:13 +0100 Subject: [PATCH 12/17] fix(api): ensure `JsonObjectBuilder` is built before adding to response --- .../java/edu/harvard/iq/dataverse/api/AbstractApiBean.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java index 5750a646336..c8a11da06bd 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -949,7 +949,8 @@ protected Response ok(JsonValue value, JsonValue message, Long totalCount) { .add(ApiConstants.STATUS_FIELD, ApiConstants.STATUS_OK) .add(ApiConstants.MESSAGE_FIELD, message) .add(ApiConstants.TOTAL_COUNT_FIELD, totalCount) - .add(ApiConstants.DATA_FIELD, value)) + .add(ApiConstants.DATA_FIELD, value) + .build()) .type(MediaType.APPLICATION_JSON) .build(); From aba03012e437590a4b3b219e9b40862931bf46d8 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 11 Feb 2026 19:32:11 +0100 Subject: [PATCH 13/17] test(util): add `@FeatureFlag` extension for test-time feature flag manipulation Introduces `@FeatureFlag`, `FeatureFlagExtension`, and `FeatureFlagBroker` to enable reliable, serial test execution with configurable feature flags via system properties, backed by `@LocalFeatureFlags` for local JVM testing. Also adds `getScopedKey()` to `FeatureFlags`, as necessary for retrieval in the extension. --- .../iq/dataverse/settings/FeatureFlags.java | 11 +++ .../dataverse/util/testing/FeatureFlag.java | 62 ++++++++++++ .../util/testing/FeatureFlagBroker.java | 42 ++++++++ .../util/testing/FeatureFlagExtension.java | 97 +++++++++++++++++++ .../util/testing/LocalFeatureFlags.java | 46 +++++++++ 5 files changed, 258 insertions(+) create mode 100644 src/test/java/edu/harvard/iq/dataverse/util/testing/FeatureFlag.java create mode 100644 src/test/java/edu/harvard/iq/dataverse/util/testing/FeatureFlagBroker.java create mode 100644 src/test/java/edu/harvard/iq/dataverse/util/testing/FeatureFlagExtension.java create mode 100644 src/test/java/edu/harvard/iq/dataverse/util/testing/LocalFeatureFlags.java diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java b/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java index 78871ff8459..1f3eb12fe74 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java @@ -299,5 +299,16 @@ public enum FeatureFlags { public boolean enabled() { return JvmSettings.FEATURE_FLAG.lookupOptional(Boolean.class, flag).orElse(defaultStatus); } + + /** + * Returns the scoped configuration key for this feature flag. + * The key is constructed by inserting the flag name into the {@link JvmSettings#FEATURE_FLAG} pattern, + * resulting in a string of the form "dataverse.feature.{flag}". + * + * @return the scoped configuration key as a String + */ + public String getScopedKey() { + return JvmSettings.FEATURE_FLAG.insert(flag); + } } diff --git a/src/test/java/edu/harvard/iq/dataverse/util/testing/FeatureFlag.java b/src/test/java/edu/harvard/iq/dataverse/util/testing/FeatureFlag.java new file mode 100644 index 00000000000..5c4f95a1082 --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/util/testing/FeatureFlag.java @@ -0,0 +1,62 @@ +package edu.harvard.iq.dataverse.util.testing; + +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.parallel.ResourceAccessMode; +import org.junit.jupiter.api.parallel.ResourceLock; +import org.junit.jupiter.api.parallel.Resources; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +/** + * {@code @FeatureFlag} is a JUnit Jupiter extension to set the value of a + * feature flag (internally a system property) for a test execution. + * + *

The name and state of the feature flag to be set must be specified via + * {@link #flag()} and {@link #value()}. After the annotated method has been + * executed, the initial default value is restored.

+ * + *

{@code @FeatureFlag} can be used on the method and on the class level. + * It is repeatable and inherited from higher-level containers. If a class is + * annotated, the configured flag will be set before every test inside that + * class. Any method level configurations will override the class level + * configurations.

+ * + * Parallel execution of tests using this extension is prohibited by using + * resource locking provided by JUnit5 - system properties are a global state, + * these tests NEED to be done serial. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Inherited +@Repeatable(FeatureFlag.FeatureFlags.class) +@ExtendWith(FeatureFlagExtension.class) +@ResourceLock(value = Resources.SYSTEM_PROPERTIES, mode = ResourceAccessMode.READ_WRITE) +public @interface FeatureFlag { + + /** + * The name of the feature flag to be set. + */ + edu.harvard.iq.dataverse.settings.FeatureFlags flag(); + + /** + * The state of the flag to be set. + */ + boolean value() default true; + + /** + * Containing annotation of repeatable {@code @FeatureFlag}. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.METHOD, ElementType.TYPE }) + @Inherited + @ExtendWith(FeatureFlagExtension.class) + @interface FeatureFlags { + FeatureFlag[] value(); + } +} diff --git a/src/test/java/edu/harvard/iq/dataverse/util/testing/FeatureFlagBroker.java b/src/test/java/edu/harvard/iq/dataverse/util/testing/FeatureFlagBroker.java new file mode 100644 index 00000000000..87b00117853 --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/util/testing/FeatureFlagBroker.java @@ -0,0 +1,42 @@ +package edu.harvard.iq.dataverse.util.testing; + +import edu.harvard.iq.dataverse.settings.FeatureFlags; + +import java.io.IOException; + +/** + * Provide an interface to access and manipulate {@link edu.harvard.iq.dataverse.settings.FeatureFlags} + * at some place (local, remote, different ways to access, etc.). + * Part of the {@link FeatureFlagExtension} extension to allow JUnit5 tests to manipulate these + * settings, enabling to test different code paths and so on. + * @implNote Keep in mind to use methods that do not require restarts or similar to set or delete a setting. + * This must be changeable on the fly, otherwise it will be useless for testing. + * Yes, non-hot-reloadable settings may be a problem. The code should be refactored in these cases. + */ +public interface FeatureFlagBroker { + + /** + * Receive the status of a {@link edu.harvard.iq.dataverse.settings.FeatureFlags}. + * @param flag The feature flag to receive + * @return The status of the flag (false = disabled, true = enabled, null = not set) + * @throws IOException When communication goes sideways. + */ + Boolean get(FeatureFlags flag) throws IOException; + + /** + * Set the state of a {@link edu.harvard.iq.dataverse.settings.FeatureFlags}. + * @param flag The feature flag to set + * @param value The flag state we want to have it set to. + * @throws IOException When communication goes sideways. + */ + void set(FeatureFlags flag, boolean value) throws IOException; + + /** + * Remove the state of a {@link edu.harvard.iq.dataverse.settings.FeatureFlags}. + * For some tests, one might want to clear a certain setting again and potentially have it set back afterward. + * @param flag The feature flag to delete. + * @throws IOException When communication goes sideways. + */ + void delete(FeatureFlags flag) throws IOException; + +} \ No newline at end of file diff --git a/src/test/java/edu/harvard/iq/dataverse/util/testing/FeatureFlagExtension.java b/src/test/java/edu/harvard/iq/dataverse/util/testing/FeatureFlagExtension.java new file mode 100644 index 00000000000..da06c6c0a3d --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/util/testing/FeatureFlagExtension.java @@ -0,0 +1,97 @@ +package edu.harvard.iq.dataverse.util.testing; + +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.AfterTestExecutionCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.platform.commons.support.AnnotationSupport; + +import java.util.List; + +public class FeatureFlagExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback, BeforeAllCallback, AfterAllCallback { + + @Override + public void beforeAll(ExtensionContext extensionContext) throws Exception { + List flags = AnnotationSupport.findRepeatableAnnotations(extensionContext.getTestClass(), FeatureFlag.class); + ExtensionContext.Store store = extensionContext.getStore( + ExtensionContext.Namespace.create(getClass(), extensionContext.getRequiredTestClass())); + + setFlag(extensionContext.getRequiredTestClass(), flags, getBroker(extensionContext), store); + } + + @Override + public void afterAll(ExtensionContext extensionContext) throws Exception { + List flags = AnnotationSupport.findRepeatableAnnotations(extensionContext.getTestClass(), FeatureFlag.class); + ExtensionContext.Store store = extensionContext.getStore( + ExtensionContext.Namespace.create(getClass(), extensionContext.getRequiredTestClass())); + + resetFlag(flags, getBroker(extensionContext), store); + } + + @Override + public void beforeTestExecution(ExtensionContext extensionContext) throws Exception { + List flags = AnnotationSupport.findRepeatableAnnotations(extensionContext.getTestMethod(), FeatureFlag.class); + ExtensionContext.Store store = extensionContext.getStore( + ExtensionContext.Namespace.create( + getClass(), + extensionContext.getRequiredTestClass(), + extensionContext.getRequiredTestMethod() + )); + + setFlag(extensionContext.getRequiredTestClass(), flags, getBroker(extensionContext), store); + } + + @Override + public void afterTestExecution(ExtensionContext extensionContext) throws Exception { + List flags = AnnotationSupport.findRepeatableAnnotations(extensionContext.getTestMethod(), FeatureFlag.class); + ExtensionContext.Store store = extensionContext.getStore( + ExtensionContext.Namespace.create( + getClass(), + extensionContext.getRequiredTestClass(), + extensionContext.getRequiredTestMethod() + )); + + resetFlag(flags, getBroker(extensionContext), store); + } + + private void setFlag(Class testClass, List flags, FeatureFlagBroker broker, ExtensionContext.Store store) throws Exception { + for (FeatureFlag flag : flags) { + // get the current state + Boolean oldState = broker.get(flag.flag()); + + // if present - store in context to restore later + if (oldState != null) { + store.put(flag, oldState); + } + + broker.set(flag.flag(), flag.value()); + } + } + + private void resetFlag(List flags, FeatureFlagBroker broker, ExtensionContext.Store store) throws Exception { + for (FeatureFlag flag : flags) { + // get a stored setting from context + Boolean oldState = store.remove(flag, Boolean.class); + + // if present before, restore + if (oldState != null) { + broker.set(flag.flag(), oldState); + // if NOT present before, delete + } else { + broker.delete(flag.flag()); + } + } + } + + private FeatureFlagBroker getBroker(ExtensionContext extensionContext) throws Exception { + // Is this test class using local system properties, then get a broker for these + if (AnnotationSupport.isAnnotated(extensionContext.getTestClass(), LocalFeatureFlags.class)) { + return LocalFeatureFlags.localBroker; + // NOTE: this might be extended later with other annotations to support other means of handling the settings + } else { + throw new IllegalStateException("You must provide the @LocalFeatureFlags annotation to the test class"); + } + } + +} diff --git a/src/test/java/edu/harvard/iq/dataverse/util/testing/LocalFeatureFlags.java b/src/test/java/edu/harvard/iq/dataverse/util/testing/LocalFeatureFlags.java new file mode 100644 index 00000000000..b9717a4816a --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/util/testing/LocalFeatureFlags.java @@ -0,0 +1,46 @@ +package edu.harvard.iq.dataverse.util.testing; + +import edu.harvard.iq.dataverse.settings.FeatureFlags; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation expresses that a test class wants to manipulate local + * settings (because the tests run within the same JVM as the code itself). + * This is mostly true for unit tests. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE }) +@ExtendWith(FeatureFlagExtension.class) +@Inherited +public @interface LocalFeatureFlags { + + FeatureFlagBroker localBroker = new FeatureFlagBroker() { + @Override + public Boolean get(FeatureFlags flag) { + // In case the setting wasn't set by us, return null. + if (System.getProperty(flag.getScopedKey()) == null) { + return null; + } + + // Otherwise: lookup via MPCONFIG as we need to take the default state into account, which is not exposed. + return flag.enabled(); + } + + @Override + public void set(FeatureFlags flag, boolean value) { + System.setProperty(flag.getScopedKey(), String.valueOf(value)); + } + + @Override + public void delete(FeatureFlags flag) { + System.clearProperty(flag.getScopedKey()); + } + }; + +} \ No newline at end of file From 83c484bfbd6749b5bb108eb5f7e5da9613125853 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 11 Feb 2026 19:32:24 +0100 Subject: [PATCH 14/17] test(api): add `@FeatureFlag`-annotated test for unified message style Adds `testUnifiedMessageStyle()` to verify `ok(String)` respects the `UNIFY_API_RESPONSE_MESSAGE_STYLE` feature flag and returns message at top level. Also updates test class to use `@LocalFeatureFlags` and modern test style (package-private methods). --- .../iq/dataverse/api/AbstractApiBeanTest.java | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/api/AbstractApiBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/api/AbstractApiBeanTest.java index c67dfeeadfa..86616946076 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/AbstractApiBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/AbstractApiBeanTest.java @@ -5,6 +5,10 @@ import java.util.HashMap; import java.util.Map; import java.util.logging.Logger; + +import edu.harvard.iq.dataverse.settings.FeatureFlags; +import edu.harvard.iq.dataverse.util.testing.FeatureFlag; +import edu.harvard.iq.dataverse.util.testing.LocalFeatureFlags; import jakarta.json.Json; import jakarta.json.JsonObject; import jakarta.json.JsonReader; @@ -18,19 +22,20 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -public class AbstractApiBeanTest { +@LocalFeatureFlags +class AbstractApiBeanTest { private static final Logger logger = Logger.getLogger(AbstractApiBeanTest.class.getCanonicalName()); AbstractApiBeanImpl sut; @BeforeEach - public void before() { + void before() { sut = new AbstractApiBeanImpl(); } @Test - public void testParseBooleanOrDie_ok() throws Exception { + void testParseBooleanOrDie_ok() throws Exception { assertTrue(sut.parseBooleanOrDie("1")); assertTrue(sut.parseBooleanOrDie("yes")); assertTrue(sut.parseBooleanOrDie("true")); @@ -50,7 +55,7 @@ void testFailIfNull_ok() { } @Test - public void testMessagesNoJsonObject() { + void testMessagesNoJsonObject() { String message = "myMessage"; Response response = sut.ok(message); JsonReader jsonReader = Json.createReader(new StringReader((String) response.getEntity().toString())); @@ -65,6 +70,21 @@ public void testMessagesNoJsonObject() { logger.info(sw.toString()); assertEquals(message, jsonObject.getJsonObject("data").getString("message")); } + + @Test + @FeatureFlag(flag = FeatureFlags.UNIFY_API_RESPONSE_MESSAGE_STYLE) + void testUnifiedMessageStyle() { + // given + String message = "myMessage"; + + // when + Response response = sut.ok(message); + + // then + JsonReader jsonReader = Json.createReader(new StringReader(response.getEntity().toString())); + JsonObject jsonObject = jsonReader.readObject(); + assertEquals(message, jsonObject.getString(ApiConstants.MESSAGE_FIELD)); + } /** * dummy implementation From f58b469d7b5eaf9832a8e3be0e380abe46ac3405 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 11 Feb 2026 20:34:53 +0100 Subject: [PATCH 15/17] docs(api,configuration): document message field style with new unified format and feature flag opt-in #11667 --- doc/sphinx-guides/source/api/changelog.rst | 10 ++++- .../source/installation/config.rst | 38 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/api/changelog.rst b/doc/sphinx-guides/source/api/changelog.rst index 5c339dea925..234566fd5b3 100644 --- a/doc/sphinx-guides/source/api/changelog.rst +++ b/doc/sphinx-guides/source/api/changelog.rst @@ -10,7 +10,15 @@ This API changelog is experimental and we would love feedback on its usefulness. v6.10 ----- -- Several API endpoints that return both a ``message`` and ``data`` field were incorrectly returning the message as a nested object (``{"message":{"message":"..."}}``). This has been fixed so that the message is now a plain string (``{"message":"..."}``). If you have integrations that depend on the old behavior, you can temporarily revert by setting ``dataverse.feature.api-message-field-legacy=true``. This flag will be removed in a future version. Affected endpoints: ``POST /api/datasets/{id}/add`` (duplicate file warning), ``PUT /api/admin/settings``, ``PUT /api/dataverses/{id}``, ``PUT /api/dataverses/{id}/inputLevels``, ``POST /api/admin/savedsearches``, ``PUT /api/harvest/clients/{nickName}``, ``PUT /api/harvest/server/oaisets/{specname}``. See `#12096 `_. +- Several API endpoints that return both a ``message`` and ``data`` field were incorrectly returning the message as a nested object (``{"message":{"message":"..."}}``). + This has been fixed so that the message is now a plain string (``{"message":"..."}``). + If you have integrations that depend on the old behavior, you can temporarily revert by setting ``dataverse.feature.api-message-field-legacy=true``. + This flag will be removed in a future version. + Affected endpoints: ``POST /api/datasets/{id}/add`` (duplicate file warning), ``PUT /api/admin/settings``, ``PUT /api/dataverses/{id}``, ``PUT /api/dataverses/{id}/inputLevels``, ``POST /api/admin/savedsearches``, ``PUT /api/harvest/clients/{nickName}``, ``PUT /api/harvest/server/oaisets/{specname}``. + See `#12096 `_. +- Most API endpoints that return a success notification but no actual data have it embedded into ``data``: ``{"data":{"message":"..."}}``. + For now, this style will remain the supported default. In a future version of Dataverse the ``message`` will always be a separate top field: ``{"data":{},"message":"..."}``. + Integrators and client vendors are welcome to opt-in to the new style and test thoroughly by enabling :ref:`dataverse.feature.unify-api-response-message-style`. v6.9 ---- diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 07084d8b126..988cb5cf588 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -3388,6 +3388,31 @@ Can also be set via any `supported MicroProfile Config API source`_, e.g. the en This setting will be ignored unless the :ref:`dataverse.api.blocked.policy` is set to ``unblock-key``. Otherwise the deprecated :ref:`:BlockedApiKey` will be used +.. _dataverse.legacy.api-response-message-style: + +dataverse.legacy.api-response-message-style ++++++++++++++++++++++++++++++++++++++++++++ + +Opt-out of no longer nesting an object in the "message" field, carrying the actual notification in its "message" field. +Enabling this will re-activate the legacy message style using ``{"message":{"message":"..."}}``, instead of the aligned format ``{"message": "..."}``. + +This option is provided as a temporary workaround for integrations that may have implemented +workarounds for the buggy behavior. The following endpoints are affected: + +- ``POST /api/datasets/{id}/add`` (just the duplicate file warning) +- ``PUT /api/admin/settings`` +- ``PUT /api/dataverses/{id}`` +- ``PUT /api/dataverses/{id}/inputLevels`` +- ``POST /api/admin/savedsearches`` +- ``PUT /api/harvest/clients/{nickName}`` +- ``PUT /api/harvest/server/oaisets/{specname}`` + +Please update your integrations to expect the corrected message format and deactivate this setting. +In a future version of Dataverse, the legacy format is expected to be removed completely. +See also :ref:`dataverse.feature.unify-api-response-message-style`. + +Can also be set via any `supported MicroProfile Config API source`_, e.g. the environment variable ``DATAVERSE_LEGACY_API_RESPONSE_MESSAGE_STYLE``. + .. _dataverse.ui.show-validity-label-when-published: dataverse.ui.show-validity-label-when-published @@ -3933,6 +3958,19 @@ dataverse.feature.api-bearer-auth-use-oauth-user-on-id-match Allows the use of an OAuth user account (GitHub, Google, or ORCID) when an identity match is found during API bearer authentication. This feature enables automatic association of an incoming IdP identity with an existing OAuth user account, bypassing the need for additional user registration steps. This feature only works when the feature flag ``api-bearer-auth`` is also enabled. **Caution: Enabling this flag could result in impersonation risks if (and only if) used with a misconfigured IdP.** +.. _dataverse.feature.unify-api-response-message-style: + +dataverse.feature.unify-api-response-message-style +++++++++++++++++++++++++++++++++++++++++++++++++++ + +When activated, the "message" in API responses will no longer be nested in the "data" field. +For any response carrying a notification, these will be found within a top-level "message" field of the JSON returned. +This affects about 230 endpoints and is likely to break existing integrations and clients. +It is mandatory to test instance clients and integrations thoroughly and it is not recommended to be used in production. +In a future Dataverse version, the (currently) experimental response message style will be made the only supported one. + +See also :ref:`dataverse.legacy.api-response-message-style`. + .. _dataverse.feature.avoid-expensive-solr-join: dataverse.feature.avoid-expensive-solr-join From 8f80e9c610590f7cb251bb3877869c5d0d2fca3d Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 11 Feb 2026 21:19:31 +0100 Subject: [PATCH 16/17] fix(api): remove trailing space from MESSAGE_FIELD constant --- src/main/java/edu/harvard/iq/dataverse/api/ApiConstants.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/ApiConstants.java b/src/main/java/edu/harvard/iq/dataverse/api/ApiConstants.java index 5e3d5a696e9..3d99d294729 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/ApiConstants.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/ApiConstants.java @@ -14,7 +14,7 @@ private ApiConstants() { public static final String DATA_FIELD = "data"; public static final String TOTAL_COUNT_FIELD = "totalCount"; - public static final String MESSAGE_FIELD = "message "; + public static final String MESSAGE_FIELD = "message"; // Authentication public static final String CONTAINER_REQUEST_CONTEXT_USER = "user"; From 47dece544dca6349f9366f0332b89854933a2b32 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 11 Feb 2026 22:52:46 +0100 Subject: [PATCH 17/17] docs(release-notes): update API message field changes and unify flag naming --- .../12096-fix-ok-message-nested-object.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/doc/release-notes/12096-fix-ok-message-nested-object.md b/doc/release-notes/12096-fix-ok-message-nested-object.md index 75f8cf4fe6d..ab1e38df8b5 100644 --- a/doc/release-notes/12096-fix-ok-message-nested-object.md +++ b/doc/release-notes/12096-fix-ok-message-nested-object.md @@ -12,10 +12,16 @@ This has been fixed. The following endpoints now return the `message` field as a - `PUT /api/harvest/clients/{nickName}` - `PUT /api/harvest/server/oaisets/{specname}` -**Note:** If you have integrations that implemented workarounds for the nested `message` object, you may need to update your code to expect a plain string instead. If you need time to update your integrations, you can temporarily revert to the legacy behavior by setting the feature flag: +**Note:** If you have integrations that implemented workarounds for the nested `message` object, you may need to update your code to expect a plain string instead. +If you need time to update your integrations, you can temporarily revert to the legacy behavior by setting this JVM option: ``` -dataverse.feature.api-message-field-legacy=true +dataverse.legacy.api-response-message-style=true ``` This flag will be removed in a future version. + +**Note:** As of this version, there is also an experimental opt-in feature that will align API responses on about 230 more occassions. +In these responses, the message is embedded into the "data" field as a nested object. +If you want to test your integrations and clients, please enable the `dataverse.feature.unify-api-response-message-style` feature flag. +In a future version of Dataverse, this now experimental style is going to become the supported default.