From af6683a35d415168fcb0a0aef4c1917a53054cc2 Mon Sep 17 00:00:00 2001 From: Pavindu Lakshan Date: Fri, 2 Oct 2026 12:31:44 +0530 Subject: [PATCH] Fix ClassCastException in RAR schema validation for integer keywords Gson parses every number of a Map as a Double, so integer schema keywords such as minItems and maxItems were returned as 1.0/3.0. JSON schema validators like Vert.x cast these keywords directly to Integer, causing a ClassCastException during RAR authorization_details validation. Add parseSchemaForValidation, which returns integral numbers as Integer (or Long when out of Integer range), and use it for schemas loaded for authorization details validation. parseSchema and the API resource management responses remain unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../util/AuthorizationDetailsTypesUtil.java | 33 +++++++++ .../AuthorizationDetailsTypesUtilTest.java | 69 +++++++++++++++++++ .../src/test/resources/testng.xml | 1 + .../mgt/dao/impl/AuthorizedAPIDAOImpl.java | 2 +- 4 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 components/api-resource-mgt/org.wso2.carbon.identity.api.resource.mgt/src/test/java/org/wso2/carbon/identity/api/resource/mgt/AuthorizationDetailsTypesUtilTest.java diff --git a/components/api-resource-mgt/org.wso2.carbon.identity.api.resource.mgt/src/main/java/org/wso2/carbon/identity/api/resource/mgt/util/AuthorizationDetailsTypesUtil.java b/components/api-resource-mgt/org.wso2.carbon.identity.api.resource.mgt/src/main/java/org/wso2/carbon/identity/api/resource/mgt/util/AuthorizationDetailsTypesUtil.java index 78a18921d422..7cb413f03bec 100644 --- a/components/api-resource-mgt/org.wso2.carbon.identity.api.resource.mgt/src/main/java/org/wso2/carbon/identity/api/resource/mgt/util/AuthorizationDetailsTypesUtil.java +++ b/components/api-resource-mgt/org.wso2.carbon.identity.api.resource.mgt/src/main/java/org/wso2/carbon/identity/api/resource/mgt/util/AuthorizationDetailsTypesUtil.java @@ -19,6 +19,9 @@ package org.wso2.carbon.identity.api.resource.mgt.util; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.ToNumberPolicy; +import com.google.gson.ToNumberStrategy; import com.google.gson.reflect.TypeToken; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -36,6 +39,23 @@ public class AuthorizationDetailsTypesUtil { private static final Log LOG = LogFactory.getLog(AuthorizationDetailsTypesUtil.class); private static final Gson GSON = new Gson(); + /* + * By default, Gson parses every JSON number of a Map as a Double. JSON schema validators such as + * Vert.x cast integer keywords (e.g. minItems, maxItems) directly to Integer, hence integral values (including + * ones written as 1.0) are returned as Integer, or as Long when they exceed the Integer range. + */ + private static final ToNumberStrategy INTEGRAL_PRESERVING_NUMBER_STRATEGY = in -> { + final Number number = ToNumberPolicy.LONG_OR_DOUBLE.readNumber(in); + final double doubleValue = number.doubleValue(); + if (doubleValue >= Integer.MIN_VALUE && doubleValue <= Integer.MAX_VALUE + && doubleValue == Math.rint(doubleValue)) { + return (int) doubleValue; + } + return number; + }; + private static final Gson INTEGRAL_PRESERVING_GSON = new GsonBuilder() + .setObjectToNumberStrategy(INTEGRAL_PRESERVING_NUMBER_STRATEGY) + .create(); private static final Type SCHEMA_TYPE = new TypeToken>() { }.getType(); /** @@ -49,6 +69,19 @@ public static Map parseSchema(final String schema) { return GSON.fromJson(schema, SCHEMA_TYPE); } + /** + * Parses a JSON schema into a map structure to be used for authorization details schema validation. Unlike + * {@link #parseSchema(String)}, integral numbers are returned as {@code Integer} (or {@code Long}) instead of + * {@code Double}, as JSON schema validators expect integer keywords such as {@code minItems} to be integers. + * + * @param schema the JSON schema string to be parsed. It must be a valid JSON string. + * @return a {@code Map} representing the parsed JSON schema. + */ + public static Map parseSchemaForValidation(final String schema) { + + return INTEGRAL_PRESERVING_GSON.fromJson(schema, SCHEMA_TYPE); + } + /** * Converts a map representing a schema into its JSON string representation. * diff --git a/components/api-resource-mgt/org.wso2.carbon.identity.api.resource.mgt/src/test/java/org/wso2/carbon/identity/api/resource/mgt/AuthorizationDetailsTypesUtilTest.java b/components/api-resource-mgt/org.wso2.carbon.identity.api.resource.mgt/src/test/java/org/wso2/carbon/identity/api/resource/mgt/AuthorizationDetailsTypesUtilTest.java new file mode 100644 index 000000000000..48897670f2cd --- /dev/null +++ b/components/api-resource-mgt/org.wso2.carbon.identity.api.resource.mgt/src/test/java/org/wso2/carbon/identity/api/resource/mgt/AuthorizationDetailsTypesUtilTest.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.identity.api.resource.mgt; + +import org.testng.Assert; +import org.testng.annotations.Test; +import org.wso2.carbon.identity.api.resource.mgt.util.AuthorizationDetailsTypesUtil; + +import java.util.List; +import java.util.Map; + +/** + * Test class for {@link AuthorizationDetailsTypesUtil}. + */ +public class AuthorizationDetailsTypesUtilTest { + + private static final String SCHEMA = "{\"type\":\"object\",\"required\":[\"type\"],\"minProperties\":1," + + "\"properties\":{\"type\":{\"type\":\"string\",\"const\":\"test_type\"}," + + "\"actions\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},\"minItems\":1,\"maxItems\":3.0}," + + "\"amount\":{\"type\":\"number\",\"minimum\":0.5,\"maximum\":9999999999}}," + + "\"allOf\":[{\"maxProperties\":5}]}"; + + @Test + @SuppressWarnings("unchecked") + public void testParseSchemaForValidationPreservesIntegralValues() { + + final Map schema = AuthorizationDetailsTypesUtil.parseSchemaForValidation(SCHEMA); + final Map properties = (Map) schema.get("properties"); + final Map actions = (Map) properties.get("actions"); + final Map amount = (Map) properties.get("amount"); + final Map allOf = ((List>) schema.get("allOf")).get(0); + + Assert.assertEquals(schema.get("minProperties"), 1); + Assert.assertEquals(actions.get("minItems"), 1); + Assert.assertEquals(actions.get("maxItems"), 3); + Assert.assertEquals(allOf.get("maxProperties"), 5); + Assert.assertEquals(amount.get("minimum"), 0.5d); + Assert.assertEquals(amount.get("maximum"), 9999999999L); + } + + @Test + @SuppressWarnings("unchecked") + public void testParseSchemaKeepsExistingNumberFormat() { + + final Map schema = AuthorizationDetailsTypesUtil.parseSchema(SCHEMA); + final Map actions = (Map) ((Map) schema.get("properties")) + .get("actions"); + + Assert.assertEquals(schema.get("minProperties"), 1.0d); + Assert.assertEquals(actions.get("minItems"), 1.0d); + Assert.assertEquals(actions.get("maxItems"), 3.0d); + } +} diff --git a/components/api-resource-mgt/org.wso2.carbon.identity.api.resource.mgt/src/test/resources/testng.xml b/components/api-resource-mgt/org.wso2.carbon.identity.api.resource.mgt/src/test/resources/testng.xml index 4d8e1a82e607..aede5dbcf9bd 100644 --- a/components/api-resource-mgt/org.wso2.carbon.identity.api.resource.mgt/src/test/resources/testng.xml +++ b/components/api-resource-mgt/org.wso2.carbon.identity.api.resource.mgt/src/test/resources/testng.xml @@ -26,6 +26,7 @@ + diff --git a/components/application-mgt/org.wso2.carbon.identity.application.mgt/src/main/java/org/wso2/carbon/identity/application/mgt/dao/impl/AuthorizedAPIDAOImpl.java b/components/application-mgt/org.wso2.carbon.identity.application.mgt/src/main/java/org/wso2/carbon/identity/application/mgt/dao/impl/AuthorizedAPIDAOImpl.java index a9a95e72f490..3948e2584c4a 100644 --- a/components/application-mgt/org.wso2.carbon.identity.application.mgt/src/main/java/org/wso2/carbon/identity/application/mgt/dao/impl/AuthorizedAPIDAOImpl.java +++ b/components/application-mgt/org.wso2.carbon.identity.application.mgt/src/main/java/org/wso2/carbon/identity/application/mgt/dao/impl/AuthorizedAPIDAOImpl.java @@ -1170,7 +1170,7 @@ private AuthorizationDetailsType buildAuthorizationDetailsTypeWithSchema(ResultS final AuthorizationDetailsType authorizationDetailsType = this.buildAuthorizationDetailsType(resultSet); if (authorizationDetailsType != null) { - authorizationDetailsType.setSchema(AuthorizationDetailsTypesUtil.parseSchema(resultSet + authorizationDetailsType.setSchema(AuthorizationDetailsTypesUtil.parseSchemaForValidation(resultSet .getString(ApplicationConstants.ApplicationTableColumns.AUTHORIZATION_DETAILS_SCHEMA))); }