Repository: dhis2/dhis2-core
Pull request #20206: fix: OpenAPI small issue fixes
Author: login: jbee
Created: 2025-03-10T08:14:19Z | Merged: 2025-03-12T11:11:10Z

Fixes a couple of issues I noticed when using OpenAPI HTML.

Issues

- [x] `/api/{entity-type}/openapi.html` did not limit scope to a single entity type
- [x] open object type in new tab link broken on play as context path wasn't considered
- [x] `@OpenApi.Property` was ignored when `@JsonIgnore` is present

Improvement

- [x] operations by default list the success response code only
- [x] operations link to GH controller file it is declared in (if not merged)


Diff:
diff --git a/dhis-2/dhis-support/dhis-support-commons/src/main/java/org/hisp/dhis/commons/jackson/jsonpatch/JsonPatch.java b/dhis-2/dhis-support/dhis-support-commons/src/main/java/org/hisp/dhis/commons/jackson/jsonpatch/JsonPatch.java
index 43f9a7e98938..d37700dd4f2e 100644
--- a/dhis-2/dhis-support/dhis-support-commons/src/main/java/org/hisp/dhis/commons/jackson/jsonpatch/JsonPatch.java
+++ b/dhis-2/dhis-support/dhis-support-commons/src/main/java/org/hisp/dhis/commons/jackson/jsonpatch/JsonPatch.java
@@ -32,6 +32,7 @@
 import com.fasterxml.jackson.databind.JsonNode;
 import java.util.List;
 import lombok.Getter;
+import org.hisp.dhis.common.OpenApi;
 
 /**
  * Container class for patch operations, allows to deserialize patches in the format '[{...},
@@ -41,6 +42,7 @@
  * @author Morten Olav Hansen
  */
 @Getter
+@OpenApi.Kind("JsonPatchObjects")
 public class JsonPatch implements Patch {
   @JsonProperty private final List<JsonPatchOperation> operations;
 
diff --git a/dhis-2/dhis-support/dhis-support-commons/src/main/java/org/hisp/dhis/commons/jackson/jsonpatch/JsonPatchOperation.java b/dhis-2/dhis-support/dhis-support-commons/src/main/java/org/hisp/dhis/commons/jackson/jsonpatch/JsonPatchOperation.java
index 32a5144bc14f..32fda381573a 100644
--- a/dhis-2/dhis-support/dhis-support-commons/src/main/java/org/hisp/dhis/commons/jackson/jsonpatch/JsonPatchOperation.java
+++ b/dhis-2/dhis-support/dhis-support-commons/src/main/java/org/hisp/dhis/commons/jackson/jsonpatch/JsonPatchOperation.java
@@ -32,6 +32,7 @@
 import com.fasterxml.jackson.annotation.JsonTypeInfo;
 import com.fasterxml.jackson.core.JsonPointer;
 import lombok.Getter;
+import org.hisp.dhis.common.OpenApi;
 import org.hisp.dhis.commons.jackson.jsonpatch.operations.AddOperation;
 import org.hisp.dhis.commons.jackson.jsonpatch.operations.RemoveByIdOperation;
 import org.hisp.dhis.commons.jackson.jsonpatch.operations.RemoveOperation;
@@ -56,6 +57,7 @@
   @JsonSubTypes.Type(name = "replace", value = ReplaceOperation.class)
 })
 @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "op")
+@OpenApi.Kind("JsonPatchObjects")
 public abstract class JsonPatchOperation implements Patch {
   public static final String ADD_OPERATION = "add";
 
diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/openapi/OpenApiController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/openapi/OpenApiController.java
index 4b226a1b6ff3..39423e2d2fe3 100644
--- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/openapi/OpenApiController.java
+++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/openapi/OpenApiController.java
@@ -143,7 +143,7 @@ public void getPathOpenApiHtml(
     // for HTML X-properties must be included
     generation.setIncludeXProperties(true);
 
-    OpenApiScopingParams scope = new OpenApiScopingParams().setScope(Set.of("path./api/" + path));
+    OpenApiScopingParams scope = new OpenApiScopingParams().setScope(Set.of("path:/api/" + path));
     getHtmlWriter(response).write(renderCached(scope, generation, rendering));
   }
 
@@ -160,7 +160,7 @@ public void getPathOpenApiYaml(
       HttpServletResponse response) {
     if (notModified(request, response, generation)) return;
 
-    OpenApiScopingParams scope = new OpenApiScopingParams().setScope(Set.of("path./api/" + path));
+    OpenApiScopingParams scope = new OpenApiScopingParams().setScope(Set.of("path:/api/" + path));
     getYamlWriter(response).write(generateCached(Language.YAML, scope, generation));
   }
 
@@ -191,7 +191,7 @@ public void getPathOpenApiJson(
       HttpServletResponse response) {
     if (notModified(request, response, generation)) return;
 
-    OpenApiScopingParams scope = new OpenApiScopingParams().setScope(Set.of("path./api/" + path));
+    OpenApiScopingParams scope = new OpenApiScopingParams().setScope(Set.of("path:/api/" + path));
     getJsonWriter(response).write(generateCached(Language.JSON, scope, generation));
   }
 
diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/openapi/OpenApiGenerator.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/openapi/OpenApiGenerator.java
index 2eae21d10bf6..00876ba01389 100644
--- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/openapi/OpenApiGenerator.java
+++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/openapi/OpenApiGenerator.java
@@ -224,6 +224,8 @@ private void generateOperation(RequestMethod method, Api.Endpoint endpoint) {
         () -> {
           addTrueMember("deprecated", endpoint.getDeprecated());
           if (xProperties()) {
+            if (endpoint.getSource() != null)
+              addStringMember("x-class", endpoint.getSource().getDeclaringClass().getName());
             addStringMember("x-maturity", getMaturityTag(endpoint.getMaturity()));
             addStringMember("x-since", getSinceVersion(endpoint.getSince()));
             addStringMember("x-group", endpoint.getGroup());
diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/openapi/OpenApiObject.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/openapi/OpenApiObject.java
index 11f3a98c122a..441bc74b62d1 100644
--- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/openapi/OpenApiObject.java
+++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/openapi/OpenApiObject.java
@@ -231,6 +231,13 @@ default Map<String, String> x_classifiers() {
       return getMap("x-classifiers", JsonString.class).toMap(JsonString::string);
     }
 
+    /**
+     * @return the Java class name of the controller declaring the operation
+     */
+    default String x_class() {
+      return getString("x-class").string();
+    }
+
     default String x_entity() {
       return x_classifiers().get("entity");
     }
diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/openapi/OpenApiRenderer.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/openapi/OpenApiRenderer.java
index c4ab16cc9988..cdc0ae796f76 100644
--- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/openapi/OpenApiRenderer.java
+++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/openapi/OpenApiRenderer.java
@@ -96,7 +96,7 @@ public class OpenApiRenderer {
        --color-tooltip: #444;
        --color-tooltiptext: #eee;
        --color-tooltipborder: lightgray;
-       --color-target: #2A303A;
+       --color-target: blue;
        --width-nav: 360px;
    }
   html {
@@ -119,11 +119,13 @@ public class OpenApiRenderer {
   h4 { font-weight: normal; padding: 0 1em; }
   nav > summary { margin: 1em 0 0.5em 0; font-weight: normal; font-size: 85%; }
 
-  h2 a[target="_blank"] { text-decoration: none; margin-right: 2em; float: right; }
+  h2 a[onclick] { text-decoration: none; margin-right: 2em; float: right; }
   a[href^="#"] { text-decoration: none; }
   a[title="permalink"] { position: absolute;  right: 1em; display: inline-block; width: 24px; height: 24px;
     text-align: center; vertical-align: middle; border-radius: 50%; line-height: 24px; color: dimgray; margin-top: -0.125rem; }
   a:not([href]) { color: blue; cursor: pointer; }
+  a.gh { display: inline-block; width: 24px; height: 24px; color: transparent; background-size: 24px 24px;
+  background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAQFHpUWHRSYXcgcHJvZmlsZSB0eXBlIGV4aWYAAHjarZlpdty6koT/YxW9BMzDcjCe0zvo5fcXIGXLlvyub592WWKJRRJAZmREJMrs//nvY/6LfzE6b2IqNbecLf9ii8133lT7/Gv3t7Px/r7/wnw/c7+eNz8+8JwKuvL5s/T3+s759POGjzHc+PW8qe8nvr4Pej/4eGDQyJ436/MkOe+f8y6+D2r7eZNbLZ+nOvxznO+FdyrvT5nP7T69s+dv8/lELERpJQYK3u/ggr2/6zODoJ8UOsfAb33KfN/3yXCIwb4zISC/LO/jaO3nAP0S5I935vfo/3j3W/B9f8+H32KZ3xjx5tsPXPo++DfEn+HwY0b+1w9scl+X8/6cs+o5+1ldj5mI5hdR1nxER/dw4SDk4d6WeRV+Eu/LfTVe1XarnC077eA1XXOeuB/joluuu+P2PU43mWL02xeO3k8f7rkaim9+BuUp6uWOL6GFFSq5nH4b0heD/zEXd8dtd7zpKiMvx6Xe8TDHLX98mf/04b95mXNUbM7Z+iNWzMsL10xDmdNvriIh7rx5SzfAH683/fYTfoAqGUw3zJUFdjueR4wnlw+2ws1z4LrE8SkhZ8p6H0CIGDsxGRfIgM3g3mVni/fFOeJYSVBn5j5EP8iAS8kvJuljCNmb4qvX2NxT3L3WJ5+9TsNNJCKFHAq5aaGTrBgT+CmxgqGeQooppZxKqia11HPIMaecc8kiuV5CiSWVXEqppZVeQ4011VxLrbXV3nwLcGBquZVWW2u9e9MZqPOszvWdM8OPMOJII48y6mijT+Az40wzzzLrbLMvv8KCJlZeZdXVVt/ObJhix5123mXX3XY/YO2EE086+ZRTTzv9R9berH55/YusuTdr/mZK15UfWeOsKeXjEU50kpQzMuajI+NFGQDQXjmz1cXolTnlzDZPUSTPJJNyY5ZTxkhh3M6n437k7mfm/ipvJtW/ypv/p8wZpe7/I3OG1H3N2zdZW9K5eTP2VKFiagPVx+e7duNrl6j1fzoOpprm8TEDmh5gJiuKPMf2tteIZhL9UROBmCNH12bdozHFFvf0o7WCpsdyMqs8p20HESzqq54hcjtwWjhnbGtS2MlyQ8ilFxbQvT8sck1S5UYJZ+U+eVDgw8CbHZjFKn2BhVhnVu2Sl8nSJqJXh+07p/PewOAEhKcQHchirVpWy95qiM3MNYRjiJF367s2bycC2Yfdo2bXup8OHuY4evh96sl9Xd5Obs0N3PqKZrQVPG5mu7b98Qj2hroAVj09RXsUYUfWdyLCIDRuqwhb21MmJLHffMRgbL+psf+HY5+rrx5bGCzKxEWRFOvqZtWpr3T23CNqCUkjV8jguJ6pYLLCh40JDU7uXA55jZmfmrLx17IFL9XzFMgGrQzZAfOQd3qNikMifMt80mr9Dm7mlxNS0gkrA9/Cb8qAx7u0kwbtvqTImHZysBrTLorx5DgJvqEEWEAIZ7tEopInhyTixKT6yCWBLgANLZV7ZV05pZUIjQ0DlVgDwvAYrQ1aYmfSpXjoK/IfKOiQ5Ov+cIQoqFayvQiEz7tWgx8DeeR6OfEH6J3ioxnjaqdVwgsLhHyAXl6uQW0zOtDdqL3oIQN0fhYY0qW6Su6Qy8wFxtOQiNszdvh7NJgvH9QNt4W2rJ91gDg3dwgiMmAnl/NNfBQeEzvYWS7NNACxinDiSyzUlPrMY4yUCUK5A/gErU24ICmoPUJvd+Le1WTuGuTT3mNq4sbcdo4WSvGE4cxpdXOEN9MZaVJA64xChWi2aZR8rDkLp50qf0BPDXjXfeJCS7JsVUcumBiLSUnz0Pp7bU8gSEVyqUEXwx1z4gzCB4XA763JxAz+I4UsulCQSs+5wdfhqVD45GvIzZ9y0ZofpxUXBrMfKTYe9wB5irHsw1j6DXJix/oN19uTa0xNLPIs8MVCBT1YXx6BoXQQuwJKlo2TEnCEfKAKtQpmCUIPhsosGwKgOsYQ/4HOeUrnyqnQep6j0CJRCq2TJ4qtpf3y0F1E68vstTYR8bUm3BU1F5I6nN2oM0QKeM85iT+PQqradH0Q1poE50hhOdJAbINBLk4f8PgDAPB/cQAX7HfUOYcvYKIxvdkRu7xnZ25tpixeWCfX6kxOawyWXyrsOxacnXDXB+NQwRQ4jiXTa6UgeYI6mtSCq05uRAn6w2/D4NlkFCquKa5A284W6RNFD6rXWsoK+pEsfLk1kRLGQIB0HjE5my4PhQI6+CMJ1yrN7RgaUwO18CXZWz0BXnJ6AfcFbxEe6chUJ8AMY/KalkWxNgQj4wxYTiM+jlygacXBFQ4oZof97UodK5gXP2HtKKmaeNts2hUXwjN5gwx2ypIbC3qZWeaEb0nsQhChGbf2CD6veKYcSMGWEG8c1xhGj6H+WUhpDYgOUDrtodLiwk4V3PmTSIh6bmWx+kWEuRfggEW7Bh9sA7MwQKLW3/zTK9aMiSubOr4oyvNBETe0i6JG09NsrBUM5OQt1zYTG8/L12BtzxgonkWLoywiwothV6uAyreVm49x+5x9h//VM6PokpK4SjQlk2O3B1WIOTuTB+I+063ePtvv5Uxi7ztCNX+RN/N3Nuset7BaN7MAoFiCtGjUyQ40aZeZ4NSNMfmb1PRjier4JWTvY+ZeES9BwSroKfquJhMqVOQmM8I2bqFz+awM8tTTVAOUZY9RSYnjGSo+Q3lCRpPX4YEdUnY141qsqXICaKScT74lAXgBLiS1bHtKmeE/YuQc5eMd+KcUHSRKsCAMqJaihkfLwlFPUSK3lU9T/DlBDwSf9SJ+P8Xi4/hFRb4/uoIiDHz6AtPM4IgCmZUv8n+IbCBrOTEezs9mXbkBrkd9+9pyPAm/Ywl9kDShx0Bjvsv9oDCWS/RN2pSelpupYiBGxKwX/ghYlsJDViXu6JSV4U+6nqRlUQH+lyXPXEMs5jmtKL8xRqj6JQzc6zm4CPm42Hm2fV2KVOKdCz1fXQvcDoPawidJg+JBTmhIHvGm+h7j1y+JFaQWTQU8GJLXsb3E/8Le/DsDOk6Pj6LJjA/okbB2DWxuO6GRHzhe4ZPRyKpOKEtxzT/iChbTFl96NQE2nYU78wqdEYHfyNkbu9j2xKl/F6U5YEKRIK7hyFagvJ3244md+TV4pF1XleUvvddw1Iu7BBQXace8IQCzdvwe7agnopIUKHma/vKfPM7fgfPrUSs0KobdxqEdgg1RTSa8Ag1irI2eESuMstNAu10ks7nGiR/4wlbTjCV5emA26c6Rkv4RUcL2I6YtrHqR96W4L/CsmXSSv9Q8xen2/gI9zKG8NhJ49ib1HrGkiyqI54YIKpJN62xReIiOFsUTa3+5B/sYGii9LlzU+FZ++B5l5m9gSJWtIWLJTsQCCrX4NvKyiH0biEfG1qAyxUf5sYkSwb+dJeuHdHiIYNxVJvuzl3lr9zP+klGwkjynZBPU/wwsj1pzLvT2MjpL7bIUotkpzggFO56JEcuf3mh74D9xEE0DFPNMpruf2CETeCMJR5kLNjfQOQtDOcAkqoUi4EIGE9DCEX1chc2AhsE2bQzdInOb0gR0f0EIWXZaWaMts7W5VSkaqGuWjWgjkC2EwPpwF5AineFxo8v8KYOtfWmhzB96qW5d9vdrAO3lSz54es2abZV58PL3uCqaRfoPugkjvGtQS2QsrjrCoINurGsZdPwDr0Xfj98IagjUjQT6rTQ2lqQFRIBVtJCM3O53fRWVvahyEkbKbPSU+kXzThelbmceQre25YoIm8F1EFs8bmdZE7zjojFiqggygFXHeNDban4kb28ARgcFhBBoZOViPTCU0aYzBjtiIZ1D9d2lLWqKUt6Xlbgcl1nBKNIdployshjldYtNDf+yqSVTGgWfes+zqrViLnRItDLYf1qZCtW5os0ihqMT9aVq1482aWCv6BlmQJsRTayf489GbyV7CXcudWUYznTPkCAu4lPaB1ER6tjfFqsipgANdAU5MxVtkTVL2oJADXHaU13xs/+B0Xsa+Ey5LBjdQYDg1806MX41DGwBsSLYPeBWoRRy0zcWBY4hInmlcakTVz+ZOlUwYqD3/MLLO0bQtJ2RTzkJGzfB+cjkydNB4SGztBLf7PGkAY6PcpeTTD+ycBVt1TDvvkkbBnqz3y2qeMyOjcX7UXMLpIkhwRJUQ1eWcB3pNZFPQ/mb0kYftUekb5qAXbhw+J1iYwjaJSnadEIagqx2tVAtbftAvJKI1KbPjgg2fyRYAhy0d0JD30lqLphSRVV3o7UdPjogF5hCK3h+8hfAkTYqfaapWs9c6atZ4X1svMpF9wzVq8XF78WrKYZOAAp0Czypkbvdq43WW/ytQ7oWnQ6WmK4se9UaJShT/BqjheumgaKcjOUSMrApptv0a8OFbKd+9tLm3FaJNqtdraKuq2wx5YEdUH8o8UwSCPzN11MlQkWJjgYgTZ8OVUem1cwGmTft57Cwpf0cGm2G2Sp5A1O4/u4s2J9dgUK2H8Nj67OllimvvndbHpTkgTjQuQztAJUwC9W/bvsBu9yHDAHgD63EwI7km8mkTF4TR1e3SRjNcdTWAOwV3Am03UvdDtLDOmGfqe2MVBGCJG5aTxLjT75GS+DZgc9Gneu2rHJq/zB2CZr6F+/g+VIbzAI/joIJiS1VtJ4uwtGr1QNCnHaJpcLRyI3Vyi27aJNh5K3tgAAWEmlSv4WjDZis2eraKi+0mutdPJdMhKlZRzWqsHTau21Kp0Jl75gjj4K+7L5sQIHQIU8tdTlYmysasIQNWQn9DTM2gw64L3j1dv3aaAZn3++O3s1ZLFKKp5BqZFbbffAlxOQMmaTd/Zz5jyOuoqm7vN1+AdxEQzszWvLRgOnZw9SXUjaY8mwuLX27+LudoYy8/IU6b+Vjq0Om6Fkf69EUV+/xRnSRfjkL1NyzLCLa6t2HzoflOt3GIrwdQzkcrTR9v0PEpzprOlC18S6+9vi7nc8QkYcQtWsdyupWe0doAXSrXaGJFTqVchlhMkRqx8LZGhwDH/VVQSywar2bOpgTZpzHartDLSCaGoC2c7g7k9eAx4lGqYyzN3FoPaBDCBkU2EVI/1ko0z31CwXd+pVQpgx1Pvmk0o4VIH0nOxmQf21gfk0P0o5z/iaTpGnt+wWLlmUf1VOz4qDm0eGwjnywOqQVxZ+MDGxQkwmwE11oyI2MaytNrIdkE/oh5ns4DQKN+AVEKWvbbjgPK1OyMLyns0QK0W2coz8DkzkpTtwQNWr6+VPf/A/tcljYp9m0nZttCnA2/aS+BPBLX6c2qm2Eax0y/mS3eWjcBn5dTBv0fUmURs6jDfd9hova1LLHaLvtfmGStVtFnI52aBNiAWGQe3BQg8c5e+1WkbL1vf83ZCdgdXqjDkcIVg331oZqJkv9chM1vIItX2fxeRJGs+gUNQhOHdJ0EBttoYV9llbXKB8aJCo7TfXTBSNWsBDYEIy5F903xHuaf2r7tMu3kHDzv6vnBne6LeYHAAABg2lDQ1BJQ0MgcHJvZmlsZQAAeJx9kT1Iw0AcxV9TtVIqHewg4pChOlkQFXHUKhShQqgVWnUwufQLmhiSFBdHwbXg4Mdi1cHFWVcHV0EQ/ABxF5wUXaTE/yWFFjEeHPfj3b3H3TtAaFSZZnWNAZpum5lUUszlV8TQK4III4IoemRmGbOSlIbv+LpHgK93CZ7lf+7P0acWLAYEROIZZpg28Trx1KZtcN4njrGyrBKfE4+adEHiR64rHr9xLrks8MyYmc3MEceIxVIHKx3MyqZGPEkcVzWd8oWcxyrnLc5atcZa9+QvjBT05SWu0xxCCgtYhAQRCmqooAobCVp1UixkaD/p4x90/RK5FHJVwMgxjw1okF0/+B/87tYqTox7SZEk0P3iOB/DQGgXaNYd5/vYcZonQPAZuNLb/o0GMP1Jer2txY+A6DZwcd3WlD3gcgcYeDJkU3alIE2hWATez+ib8kD/LRBe9Xpr7eP0AchSV+kb4OAQGClR9prPu3s7e/v3TKu/H+nLcnDqAyOTAAAABmJLR0QA2QAMAAx9ChMYAAAACXBIWXMAADddAAA3XQEZgEZdAAAAB3RJTUUH6QMKDA8obrU3DwAABx1JREFUaN7tmn+MXFUVxz93d7Zb11LosFNmlWCVN0vfKn0jW36oCLZCXFIkosY/KKhpVYixsYqhVoyAUfFHBI0KCVojYIlgCgkoILQFDSoCoe8Cnbvt3EKRSLswWWu73VK6O9c/3ut2mL6ZuW9/zDyiJ9lksvece+/3nXPPOfecC/9jJJq5WG9fvsMI5hlDV7j6/jbDnu0F/9CbHrDT552M4VzgdCAPvAvIRKxpgFeBIrAVeALBI7ogn088YMf1FgLLgU8BvVOcbhuwAbhVK7k9UYAd1zsPWAN8eAasxgCbBPy4qOSDLQXsuN6ZwA+Bc5p0BP8GrNZKPtlUwDnXm2sCoJ8H2prsaMvALQLWFJXcO+OAHdc7C1gfOqFW0g5guVbyH3GE2mOCXQH8PvS2raY0cFk6k901XBraMu2AHde7BrgRSCUoj0gBF6UzWTNcGvrztAF2XO87wLcSnEAtSWeys4ZLQ5unDNhxva8D170JssYPpjPZ14ZLQ3+dNGDH9T4J3NzsFHQKtDSdyRaGS0OF2F7acb1e4Elgbg2WMWBT6DwWAZ0zDOYQ8CywCzgXmFODbx/Qr5UsRg1GxlDnPfkUcHsdsAAbtZIDWskzEKIbuBR4fAaA+sDlIDJayX6t5IXAr+rwHwPcEWKI9HJH07j5MnBGg43cf/iHLvgjYWxen3O9gTApObWK/yDwMlACRsOUsQuYB7w9/F1JLwJrxg6Iu3bu9E3V2EPA6jp7W8y4+Qrwo4YmnXO9rAluLnMaOQmt5GNRAye7+Q6BWQ30IHgMhEyVeWFw0C9H8S9YkBepLt6GMR7wAaDdGPHtHYP+aPRxy58AZneD/e0DTtFK7qqrYQPXWIAFIV6umQIp/1DU161FoQb/Ff7d34h/rJNXUgcZa5ATHANcDXyp5hnOud6JwIoYt5iW0E7fN2FO3YhWOq7XUxOwgSuAWXZwTcvSS8fNz7Pc52xgVSRgZ2E+FUO7AP2tC7cmztqfrfTYRzQszFKgJ8ZEJ7QwwcjG4O1h3CyJMumLYkzyuBkX320VWq3kb4HfxRC5OArwR2JcwL+wY3vzKo01aBWwx5L3/DcAdtx8D+BYCt+tlXy21UmzVrIU5vlWfi6I3RMaNqfFWOv2BF0WfhPD0S2uNGnXUmoMITYlBW1Yvn3Rkv3dlYDfaVtH0gV/f8KuhM9Z8r2jErBtEvFKAu/AtnuaXwn4OEuh8QQCHrPkO7YScIel0FsSCNh2Tx2VgG01151AwN1xrPMw4BFLoRMdN59KGOAFlnz7KgGXLIU6wbhJQeq4+bfGSJhKlYD/GWOdc5KjXHM29o2BlyoBb4uxyscTZM5x9qIqAW+JIbjEcb1TW4001+d1A5dYCwjhTwAe6xRFgmcHVqLA9S03ZsNabGpvAb06NoqeABzWiDbHWG+Z43qfa52z8pZSv0xbTRsPl3or78P3xVz3547rXdB0U3a9foKWbZxG/B+OKgAYxL0EBXJb6gTuc1zvqoUL821N0uylBh4laO/Y0ohoExOAJ5pp/y7tfj2dyeYInhgddQYIejpzq75sG3B+WbDs+Ez2pfndPc+XSrvNDAA9O53JrgO+hm1V9Qit1wV5V6UDqpz4vcDTVQJ7hTA9xcIzoznXm2+CPvEXiW7E7QjN7WHaxBN6qz8yGYC9ffnZZWP6gfPC0LNosh9LwGlFJbdEAg5BPwAMVP37UQQrdEG+EJ6jlQZ+Sf026niY0FyrlbzNUpMDBK8MHKbnpcGDWsk3+Jmos/cNjq7qfwjDU4fjb1HJdcAvGizWDrwG4k7re16n+FN4v50OsGURtFqoC1gH6l8XMUEa2OD0LeoKJddaZGg/0co/aLvDMDxOV4xfV1Ty6YaAQztfC0R153IY8U0AvVWOgBgA6jwLFA/ETiiE2BxYxpRotwheBkZmTbXO0wXAHyN4DgAnhWVSet38nDLmCuATwEnAfkAC92gl75ikV94K9E3WlIFlusYzRdFg4e8RaLuartNKXjuD8fbvwFmTFP++VnJtrcG6CUMqMN+7I4auyrne6QmsftyTMuLqegx1AQ8O+mWMWA48XF1HMvCI43qrevvysxMCdiNCXFLrlYGVSR/JXxd1GcSGiPgM8B/gL6HzGgkzoflAh1byM00y6YcE4uKi8humxlbxrqieGXVOyX+MNnML8Omq4WOBj0YZSJM0ux7EyqJl+LNO+vU2/2CosdUEL3Kmbe5J0iHgyrED4rI4sT72prSSPxWBufkNWGdNAUwjv/Ac8H6t5A0RT5qmXwtFJX1hxJnAldj3aKeD9gapr1islXxqMhNMOmctDvqvAzfkXO82A18FLq+6p5anAKy6MbAH+DXwA63klPpb7VP95MOlodHh0tCm7kzPTSa4Hh5P8KruZ41ettaidCbbDrwvPDbXg1iplbx3uDSUtM7l/ylx9F8/ETUL0qMSnwAAAABJRU5ErkJggg==); }
   #hotkeys a { color: #eee; display: block; margin-top: 2px; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; font-size: 75%; }
   #hotkeys a:visited { color: #eee; }
   button, input { font-family: inherit; }
@@ -235,7 +237,8 @@ public class OpenApiRenderer {
   code.http.content { margin-top: -2px; color: #aaa; display: inline-grid; grid-template-columns: 1fr 1fr 1fr; vertical-align: top; text-align: center; }
   code.http.content > span { font-size: 70%; font-weight: normal; padding: 0 0.25em; margin: 1px; }
   code.http.content > span.status4xx { background: color-mix(in srgb, tomato 75%, transparent); color: snow; }
-  code.http.content > span.status2xx { background: color-mix(in srgb, seagreen 75%, transparent); color: snow; }
+  code.http.content > span.status2xx { color: black; }
+  body:not([content-]) code.http.content > span.status2xx { background: color-mix(in srgb, seagreen 75%, transparent); color: snow; }
   code.http.content .on { color: black; }
   code.http.method { width: 4rem; text-align: right; color: dimgray; }
   .desc code { background: color-mix(in srgb, snow 70%, transparent); padding: 0.125em 0.25em; }
@@ -279,8 +282,8 @@ public class OpenApiRenderer {
 
 
   /* target highlighting */
-  details.op:target > summary,
-  details.schema:target > summary,
+  details.op:target > summary code.url.path,
+  details.schema:target > summary code.type,
   details.param:target > summary > code:first-of-type,
   details.property:target > summary > code:first-of-type,
   details.request:target > summary > code:first-of-type,
@@ -327,7 +330,9 @@ public class OpenApiRenderer {
   #body[request-] .op > summary > code.request + code.type,
   #body[response-] .op > summary > code.response,
   #body[response-] .op > summary > code.response + code.type,
-  #body[content-] .op > summary > code.http.content { display: none; }
+  #body[content-] .op > summary > code.http.content:first-of-type,
+  #body[content-] .op > summary > code.http.content:not(:first-of-type) span:not(:first-child) { display: none; }
+  #body[content-] .op > summary > code.http.content { display: inline-block; }
 
   nav button {
       border: none;
@@ -342,7 +347,7 @@ public class OpenApiRenderer {
   nav button:before { content: '🞕'; margin-right: 0.5rem;font-weight: normal; }
 
   details.box aside button,
-  details.box aside .button { background-color: #444; color: white; border: none; display: inline-block;
+  details.box aside .button { background-color: #2A303A; color: white; border: none; display: inline-block;
     cursor: pointer; margin: 0 1em 0 0; padding: 0.25em 0.5em; font-size: 90%;
     box-shadow: 1px 1px 0 0, 2px 2px 0 0, 3px 3px 0 0, 4px 4px 0 0, 5px 5px 0 0;}
   details.box aside .button > summary { display: inline-block; margin: 0; padding: 0; }
@@ -452,14 +457,14 @@ function setLocationPathnameFile(name) {
     window.location.pathname = base + '/'+name;
   }
 
-  function setLocationSearch(name, value) {
+  function setLocationSearch(name, value, blank) {
     const searchParams = new URLSearchParams(window.location.search)
     if (value === '' || value == null) {
       searchParams.delete(name);
     } else {
       searchParams.set(name, value);
     }
-    setLocationSearchParams(searchParams);
+    setLocationSearchParams(searchParams, blank);
   }
 
   function removeLocationSearch(name, value) {
@@ -484,9 +489,17 @@ function modifyLocationSearch(button) {
     setLocationSearchParams(params);
   }
 
-  function setLocationSearchParams(params) {
+  function setLocationSearchParams(params, blank) {
     // undo : and / escaping for URL readability
-    window.location.search = params.toString().replaceAll('%3A', ':').replaceAll('%2F', '/');
+    const search = params.toString().replaceAll('%3A', ':').replaceAll('%2F', '/');
+    if (blank) {
+      let currentUrl = window.location.href;
+      let updatedUrl = new URL(currentUrl);
+      updatedUrl.search = search;
+      window.open(updatedUrl.href, '_blank');
+    } else {
+      window.location.search = search;
+    }
   }
 
   function addHashHotkey() {
@@ -644,7 +657,7 @@ private void renderDocument() {
               });
           appendTag(
               "body",
-              Map.of("id", "body"),
+              Map.of("id", "body", "content-", ""),
               () -> {
                 renderPageMenu();
                 renderPageHeader();
@@ -876,7 +889,10 @@ private void renderPathOperationSummary(OperationsItem op) {
         "h2",
         () -> {
           appendRaw(toWords(op.entity()));
-          appendA("/api/openapi/openapi.html?scope=entity:" + op.entity, true, "&#x1F5D7;");
+          appendA(
+              "setLocationSearch('scope', 'entity:%s', true)".formatted(op.entity),
+              "&#x1F5D7;",
+              "");
         });
   }
 
@@ -923,7 +939,16 @@ private void renderOperation(OperationObject op) {
         operationStyle(op),
         () -> {
           appendSummary(id, op.summary(), () -> renderOperationSummary(op));
-          renderBoxToolbar(null);
+          renderBoxToolbar(
+              () -> {
+                String declaringClass = op.x_class();
+                if (declaringClass != null) {
+                  String url =
+                      "https://github.com/dhis2/dhis2-core/blob/master/dhis-2/dhis-web-api/src/main/java/%s.java"
+                          .formatted(declaringClass.replace('.', '/'));
+                  appendTag("a", Map.of("href", url, "target", "_blank", "class", "gh"), "GH");
+                }
+              });
           appendTag("header", markdownToHTML(op.description(), op.parameterNames()));
           renderLabelledValue("operationId", op.operationId());
           renderLabelledValue("since", op.x_since());
@@ -1224,7 +1249,7 @@ private void renderSchemaSignatureType(List<SchemaObject> oneOf) {
 
   private void renderSchemaSignatureType(SchemaObject schema) {
     if (schema.isShared()) {
-      appendA("#" + schema.getSharedName(), false, schema.getSharedName());
+      appendA("#" + schema.getSharedName(), schema.getSharedName());
       return;
     }
     renderSchemaSignatureTypeAny(schema);
@@ -1480,7 +1505,7 @@ private void appendSummary(@CheckForNull String id, String title, Runnable body)
         "summary",
         attrs,
         () -> {
-          if (id != null) appendA("#" + id, false, "#");
+          if (id != null) appendA("#" + id, "#");
           body.run();
         });
   }
@@ -1489,10 +1514,9 @@ private void appendInputButton(String text, String onclick) {
     appendTag("input", Map.of("type", "button", "value", text, "onclick", onclick));
   }
 
-  private void appendA(String href, boolean blank, String text) {
-    String target = blank ? "_blank" : "";
+  private void appendA(String href, String text) {
     String title = "#".equals(text) ? "permalink" : "";
-    appendTag("a", Map.of("href", href, "target", target, "title", title), text);
+    appendTag("a", Map.of("href", href, "title", title), text);
   }
 
   private void appendA(String onclick, String text, String title) {
diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/openapi/Property.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/openapi/Property.java
index f26a5e420463..672c4a1a60c0 100644
--- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/openapi/Property.java
+++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/openapi/Property.java
@@ -142,8 +142,8 @@ private static List<Property> propertiesIn(Class<?> object) {
         .filter(f -> includeByDefault || isExplicitlyIncluded(f))
         .forEach(addField);
     methodsIn(object)
-        .filter(method -> Property.isGetter(method) || Property.isSetter(method))
         .filter(Property::isExplicitlyIncluded)
+        .filter(Property::isAccessor)
         .filter(method -> !ignoredFields.contains(getJavaPropertyName(method)))
         .forEach(
             method ->
@@ -151,7 +151,7 @@ private static List<Property> propertiesIn(Class<?> object) {
                     method, fieldsByJavaPropertyName.get(getJavaPropertyName(method))));
     if (properties.isEmpty() || includeByDefault) {
       methodsIn(object)
-          .filter(Property::isGetter)
+          .filter(Property::isAccessor)
           .filter(method -> !ignoredFields.contains(getJavaPropertyName(method)))
           .forEach(
               method ->
@@ -197,6 +197,10 @@ private static boolean isProperty(Field source) {
         && !source.getType().isAnnotationPresent(OpenApi.Ignore.class);
   }
 
+  private static boolean isAccessor(Method source) {
+    return isGetter(source) || isSetter(source);
+  }
+
   private static boolean isSetter(Method source) {
     String name = source.getName();
     return !isExplicitlyExcluded(source)
@@ -225,7 +229,8 @@ private static <T extends Member & AnnotatedElement> boolean isExplicitlyExclude
     return source.isSynthetic()
         || isStatic(source.getModifiers())
         || source.isAnnotationPresent(OpenApi.Ignore.class)
-        || source.isAnnotationPresent(JsonIgnore.class);
+        || (source.isAnnotationPresent(JsonIgnore.class)
+            && !source.isAnnotationPresent(OpenApi.Property.class));
   }
 
   private static boolean isExplicitlyIncluded(AnnotatedElement source) {
diff --git a/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/openapi/PropertyTest.java b/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/openapi/PropertyTest.java
index f9ebc1582fea..f3de1a774435 100644
--- a/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/openapi/PropertyTest.java
+++ b/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/openapi/PropertyTest.java
@@ -28,12 +28,15 @@
 package org.hisp.dhis.webapi.openapi;
 
 import static java.util.stream.Collectors.toMap;
+import static java.util.stream.Collectors.toSet;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.function.Function;
 import lombok.Getter;
 import lombok.Setter;
@@ -42,45 +45,61 @@
 import org.junit.jupiter.api.Test;
 
 class PropertyTest {
-  @OpenApi.Property(value = String.class)
-  private static class AProperty {}
-
-  private static class AnotherProperty {
-    @OpenApi.Property
-    public void setOpenApiProperty(String openApiProperty) {}
-
-    public void setProperty(String property) {}
-  }
 
-  private static class IgnoredProperty {
+  @OpenApi.Property(value = String.class)
+  private static class StringInOpenAPI {}
 
-    @OpenApi.Ignore @JsonProperty private String openApiProperty;
+  private static class RedefinedType {
 
-    @JsonProperty
-    public void setOpenApiProperty(String openApiProperty) {}
+    @JsonProperty private StringInOpenAPI text;
   }
 
-  @JsonProperty private AProperty aProperty;
-
   @Test
   void testGetPropertiesGivenOpenApiPropertyAnnotatedClassThatHasValueSet() {
-    Collection<Property> properties = Property.getProperties(PropertyTest.class);
+    Collection<Property> properties = Property.getProperties(RedefinedType.class);
+    assertEquals(1, properties.size());
     assertEquals(String.class, new ArrayList<>(properties).get(0).getType());
   }
 
+  /**
+   * When some properties have OpenAPI annotation but none has jackson annotations all properties
+   * are included implicitly. This is because it is assumed that the OpenAPI annotation is used to
+   * adjust the annotated property, not to select it.
+   */
+  private static class AnnotatedSetter {
+    @OpenApi.Property
+    public void setExplicit(String value) {}
+
+    public void setImplicit(String value) {}
+  }
+
   @Test
   void testGetPropertiesGivenOpenApiPropertyAnnotatedSetter() {
-    Collection<Property> properties = Property.getProperties(AnotherProperty.class);
-    assertEquals(1, properties.size());
-    Property property = new ArrayList<>(properties).get(0);
-    assertEquals("openApiProperty", property.getName());
-    assertEquals(String.class, property.getType());
+    Collection<Property> properties = Property.getProperties(AnnotatedSetter.class);
+    assertEquals(2, properties.size());
+    assertEquals(
+        Set.of("explicit", "implicit"),
+        properties.stream().map(Property::getName).collect(toSet()));
+    assertEquals(Set.of(String.class), properties.stream().map(Property::getType).collect(toSet()));
+  }
+
+  /**
+   * When a jackson annotated property is ignored using OpenAPI annotations that property is
+   * excluded. Any accessor with a OpenAPI or jackson annotation is also included.
+   */
+  private static class IgnoredField {
+
+    @OpenApi.Ignore @JsonProperty private String excluded;
+
+    @JsonProperty
+    public void setIncluded(String value) {}
   }
 
   @Test
   void testGetPropertiesGivenOpenApiIgnoreAndJsonPropertyAnnotatedField() {
-    Collection<Property> properties = Property.getProperties(IgnoredProperty.class);
-    assertEquals(0, properties.size());
+    List<Property> properties = Property.getProperties(IgnoredField.class);
+    assertEquals(1, properties.size());
+    assertEquals("included", properties.get(0).getName());
   }
 
   @Getter


Reviews:
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T09:05:25Z
user: login: jbee
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T09:23:27Z
user: login: larshelge
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-12T09:39:53Z
user: login: david-mackessy
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T09:49:24Z

Review comments:
diff_hunk: @@ -42,45 +45,61 @@
 import org.junit.jupiter.api.Test;
 
 class PropertyTest {
-  @OpenApi.Property(value = String.class)
-  private static class AProperty {}
-
-  private static class AnotherProperty {
-    @OpenApi.Property
-    public void setOpenApiProperty(String openApiProperty) {}
-
-    public void setProperty(String property) {}
-  }
 
-  private static class IgnoredProperty {
+  @OpenApi.Property(value = String.class)
+  private static class StringInOpenAPI {}
 
-    @OpenApi.Ignore @JsonProperty private String openApiProperty;
+  private static class RedefinedType {
 
-    @JsonProperty
-    public void setOpenApiProperty(String openApiProperty) {}
+    @JsonProperty private StringInOpenAPI text;
   }
 
-  @JsonProperty private AProperty aProperty;
-
   @Test
   void testGetPropertiesGivenOpenApiPropertyAnnotatedClassThatHasValueSet() {
-    Collection<Property> properties = Property.getProperties(PropertyTest.class);
+    Collection<Property> properties = Property.getProperties(RedefinedType.class);
+    assertEquals(1, properties.size());
     assertEquals(String.class, new ArrayList<>(properties).get(0).getType());
   }
 
+  /**
+   * When some properties have OpenAPI annotation but none has jackson annotations all properties
+   * are included implicitly. This is because it is assumed that the OpenAPI annotation is used to
+   * adjust the annotated property, not to select it.
+   */
+  private static class AnnotatedSetter {
+    @OpenApi.Property
+    public void setExplicit(String value) {}
path: dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/openapi/PropertyTest.java
user: login: github-advanced-security[bot]
body: ## Useless parameter

The parameter 'value' is never used.

[Show more details](https://github.com/dhis2/dhis2-core/security/code-scanning/9575)
created_at: 2025-03-12T09:05:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -42,45 +45,61 @@
 import org.junit.jupiter.api.Test;
 
 class PropertyTest {
-  @OpenApi.Property(value = String.class)
-  private static class AProperty {}
-
-  private static class AnotherProperty {
-    @OpenApi.Property
-    public void setOpenApiProperty(String openApiProperty) {}
-
-    public void setProperty(String property) {}
-  }
 
-  private static class IgnoredProperty {
+  @OpenApi.Property(value = String.class)
+  private static class StringInOpenAPI {}
 
-    @OpenApi.Ignore @JsonProperty private String openApiProperty;
+  private static class RedefinedType {
 
-    @JsonProperty
-    public void setOpenApiProperty(String openApiProperty) {}
+    @JsonProperty private StringInOpenAPI text;
   }
 
-  @JsonProperty private AProperty aProperty;
-
   @Test
   void testGetPropertiesGivenOpenApiPropertyAnnotatedClassThatHasValueSet() {
-    Collection<Property> properties = Property.getProperties(PropertyTest.class);
+    Collection<Property> properties = Property.getProperties(RedefinedType.class);
+    assertEquals(1, properties.size());
     assertEquals(String.class, new ArrayList<>(properties).get(0).getType());
   }
 
+  /**
+   * When some properties have OpenAPI annotation but none has jackson annotations all properties
+   * are included implicitly. This is because it is assumed that the OpenAPI annotation is used to
+   * adjust the annotated property, not to select it.
+   */
+  private static class AnnotatedSetter {
+    @OpenApi.Property
+    public void setExplicit(String value) {}
+
+    public void setImplicit(String value) {}
path: dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/openapi/PropertyTest.java
user: login: github-advanced-security[bot]
body: ## Useless parameter

The parameter 'value' is never used.

[Show more details](https://github.com/dhis2/dhis2-core/security/code-scanning/9574)
created_at: 2025-03-12T09:05:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -42,45 +45,61 @@
 import org.junit.jupiter.api.Test;
 
 class PropertyTest {
-  @OpenApi.Property(value = String.class)
-  private static class AProperty {}
-
-  private static class AnotherProperty {
-    @OpenApi.Property
-    public void setOpenApiProperty(String openApiProperty) {}
-
-    public void setProperty(String property) {}
-  }
 
-  private static class IgnoredProperty {
+  @OpenApi.Property(value = String.class)
+  private static class StringInOpenAPI {}
 
-    @OpenApi.Ignore @JsonProperty private String openApiProperty;
+  private static class RedefinedType {
 
-    @JsonProperty
-    public void setOpenApiProperty(String openApiProperty) {}
+    @JsonProperty private StringInOpenAPI text;
   }
 
-  @JsonProperty private AProperty aProperty;
-
   @Test
   void testGetPropertiesGivenOpenApiPropertyAnnotatedClassThatHasValueSet() {
-    Collection<Property> properties = Property.getProperties(PropertyTest.class);
+    Collection<Property> properties = Property.getProperties(RedefinedType.class);
+    assertEquals(1, properties.size());
     assertEquals(String.class, new ArrayList<>(properties).get(0).getType());
   }
 
+  /**
+   * When some properties have OpenAPI annotation but none has jackson annotations all properties
+   * are included implicitly. This is because it is assumed that the OpenAPI annotation is used to
+   * adjust the annotated property, not to select it.
+   */
+  private static class AnnotatedSetter {
+    @OpenApi.Property
+    public void setExplicit(String value) {}
+
+    public void setImplicit(String value) {}
+  }
+
   @Test
   void testGetPropertiesGivenOpenApiPropertyAnnotatedSetter() {
-    Collection<Property> properties = Property.getProperties(AnotherProperty.class);
-    assertEquals(1, properties.size());
-    Property property = new ArrayList<>(properties).get(0);
-    assertEquals("openApiProperty", property.getName());
-    assertEquals(String.class, property.getType());
+    Collection<Property> properties = Property.getProperties(AnnotatedSetter.class);
+    assertEquals(2, properties.size());
+    assertEquals(
+        Set.of("explicit", "implicit"),
+        properties.stream().map(Property::getName).collect(toSet()));
+    assertEquals(Set.of(String.class), properties.stream().map(Property::getType).collect(toSet()));
+  }
+
+  /**
+   * When a jackson annotated property is ignored using OpenAPI annotations that property is
+   * excluded. Any accessor with a OpenAPI or jackson annotation is also included.
+   */
+  private static class IgnoredField {
+
+    @OpenApi.Ignore @JsonProperty private String excluded;
+
+    @JsonProperty
+    public void setIncluded(String value) {}
path: dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/openapi/PropertyTest.java
user: login: github-advanced-security[bot]
body: ## Useless parameter

The parameter 'value' is never used.

[Show more details](https://github.com/dhis2/dhis2-core/security/code-scanning/9576)
created_at: 2025-03-12T09:05:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -142,16 +142,16 @@ private static List<Property> propertiesIn(Class<?> object) {
         .filter(f -> includeByDefault || isExplicitlyIncluded(f))
         .forEach(addField);
     methodsIn(object)
-        .filter(method -> Property.isGetter(method) || Property.isSetter(method))
         .filter(Property::isExplicitlyIncluded)
+        .filter(Property::isAccessor)
         .filter(method -> !ignoredFields.contains(getJavaPropertyName(method)))
         .forEach(
             method ->
                 addMethod.accept(
                     method, fieldsByJavaPropertyName.get(getJavaPropertyName(method))));
     if (properties.isEmpty() || includeByDefault) {
       methodsIn(object)
-          .filter(Property::isGetter)
+          .filter(Property::isAccessor)
path: dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/openapi/Property.java
user: login: jbee
body: @cjmamo Please have a look at this change again in the context of your use case. 

I checked back with the changes you had made a while back to the properties selection logic. Back then you did also consider both getters and setters and I think I changed that later to just getters. As far as I can recall I did that thinking setters were unnecessary. But that is not the case. Sometimes we use setters to allow the property input to be another type or to be given using an alias and similar.
created_at: 2025-03-12T09:23:26Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-03-12T08:55:01Z
body: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=dhis2_dhis2-core&pullRequest=20206) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [3 New issues](https://sonarcloud.io/project/issues?id=dhis2_dhis2-core&pullRequest=20206&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [3 New Code Smells](https://sonarcloud.io/dashboard?id=dhis2_dhis2-core&pullRequest=20206) (required ≤ 0)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [3 New Critical Issues](https://sonarcloud.io/dashboard?id=dhis2_dhis2-core&pullRequest=20206) (required ≤ 0)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=dhis2_dhis2-core&pullRequest=20206)

##   
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/light_bulb-16px.png '') Catch issues before they fail your Quality Gate with our IDE extension ![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/sonarlint-16px.png '') [SonarQube for IDE](https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=pull-request)


author_association: NONE