This is an automated email from the ASF dual-hosted git repository.

Lukas-Finster pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git

commit 93f0b9711956417d08677409898f378ee35764eb
Author: Lukas Finster <[email protected]>
AuthorDate: Mon Jul 27 11:19:51 2026 +0200

    Improved: Added support for DomainModelObject in rest-api (OFBIZ-12517)
    
    * Fascilitating transformation of rawData to full java-objects
---
 .../base/conversion/AbstractModelConverter.java    |  43 ++++
 .../ofbiz/base/conversion/ModelConverter.java      |  45 ++++
 .../ofbiz/base/conversion/ModelConverters.java     | 102 +++++++++
 .../org/apache/ofbiz/base/model/DomainModel.java   |  23 ++
 .../org/apache/ofbiz/base/util/ObjectType.java     |  10 +
 .../base/conversion/TestDomainModelConversion.java | 234 +++++++++++++++++++++
 ...rg.apache.ofbiz.base.conversion.ConverterLoader |   1 +
 framework/rest-api/dtd/rest-api.xsd                |   7 +
 .../org/apache/ofbiz/ws/rs/model/ModelApi.java     |  27 ++-
 .../apache/ofbiz/ws/rs/model/ModelApiReader.java   |   9 +
 .../org/apache/ofbiz/ws/rs/model/ModelMapping.java |  71 +++++++
 .../apache/ofbiz/ws/rs/model/ModelQueryParam.java  |  80 +++++++
 .../ofbiz/ws/rs/openapi/OFBizOpenApiReader.java    |  14 +-
 .../org/apache/ofbiz/ws/rs/util/OpenApiUtil.java   |  92 +++++++-
 .../apache/ofbiz/ws/rs/util/OpenApiUtilTest.java   |  75 +++++++
 15 files changed, 826 insertions(+), 7 deletions(-)

diff --git 
a/framework/base/src/main/java/org/apache/ofbiz/base/conversion/AbstractModelConverter.java
 
b/framework/base/src/main/java/org/apache/ofbiz/base/conversion/AbstractModelConverter.java
new file mode 100644
index 0000000000..8feb151b23
--- /dev/null
+++ 
b/framework/base/src/main/java/org/apache/ofbiz/base/conversion/AbstractModelConverter.java
@@ -0,0 +1,43 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.ofbiz.base.conversion;
+
+
+/** Abstract ModelConverter class. This class handles converter registration
+ * and it implements the <code>canConvert</code>, <code>getSourceClass</code>,
+ * and <code>getTargetClass</code> methods.
+ */
+public abstract class AbstractModelConverter<S, T> extends 
AbstractConverter<S, T> implements ModelConverter<S, T> {
+
+    protected AbstractModelConverter(Class<? super S> sourceClass, Class<? 
super T> targetClass) {
+        super(sourceClass, targetClass);
+    }
+
+    /**
+     * Converts <code>obj</code> to <code>T</code>.
+     *
+     * @param targetClass
+     * @param obj
+     * @param targetClassName
+     * @return The converted <code>Object</code>
+     */
+    public T convert(Class<? extends T> targetClass, S obj, String 
targetClassName) throws ConversionException {
+        return convert(obj, targetClassName);
+    }
+}
diff --git 
a/framework/base/src/main/java/org/apache/ofbiz/base/conversion/ModelConverter.java
 
b/framework/base/src/main/java/org/apache/ofbiz/base/conversion/ModelConverter.java
new file mode 100644
index 0000000000..8eb68750ca
--- /dev/null
+++ 
b/framework/base/src/main/java/org/apache/ofbiz/base/conversion/ModelConverter.java
@@ -0,0 +1,45 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.ofbiz.base.conversion;
+
+/** Model converter interface. Classes implement this interface
+ * to convert one object type to another.
+ */
+public interface ModelConverter<S, T> extends Converter<S, T> {
+
+    /** Converts <code>obj</code> to <code>T</code>.
+     *
+     * @param obj The source <code>Object</code> to convert
+     * @param targetClassName The target class name for conversion - must not 
be <code>null</code>
+     * @return The converted <code>Object</code>
+     * @throws ConversionException
+     */
+    T convert(S obj, String targetClassName) throws ConversionException;
+
+    /** Converts <code>obj</code> to <code>T</code>.
+     *
+     * @param targetClass The <code>Class</code> to convert to
+     * @param obj The source <code>Object</code> to convert
+     * @param targetClassName The target class name for conversion - must not 
be <code>null</code>
+     * @return The converted <code>Object</code>
+     * @throws ConversionException
+     */
+    T convert(Class<? extends T> targetClass, S obj, String targetClassName) 
throws ConversionException;
+
+}
diff --git 
a/framework/base/src/main/java/org/apache/ofbiz/base/conversion/ModelConverters.java
 
b/framework/base/src/main/java/org/apache/ofbiz/base/conversion/ModelConverters.java
new file mode 100644
index 0000000000..dc5a302b79
--- /dev/null
+++ 
b/framework/base/src/main/java/org/apache/ofbiz/base/conversion/ModelConverters.java
@@ -0,0 +1,102 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.ofbiz.base.conversion;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.ofbiz.base.lang.JSON;
+import org.apache.ofbiz.base.model.DomainModel;
+import org.apache.ofbiz.base.util.GeneralException;
+import org.apache.ofbiz.base.util.ObjectType;
+import org.apache.ofbiz.base.util.UtilGenerics;
+import org.apache.ofbiz.base.util.UtilValidate;
+
+/** Model Converter classes. */
+public class ModelConverters implements ConverterLoader {
+
+    public static class HashMapToDomainModel extends 
AbstractModelConverter<LinkedHashMap<String, Object>, DomainModel> {
+
+        public HashMapToDomainModel() {
+            super(LinkedHashMap.class, DomainModel.class);
+        }
+
+        @Override
+        public DomainModel convert(LinkedHashMap<String, Object> obj, String 
targetClassName) throws ConversionException {
+            try {
+                Class<?> modelClass = Class.forName(targetClassName);
+                JSON jsonObj = JSON.from(obj);
+                DomainModel target = 
UtilGenerics.cast(jsonObj.toObject(modelClass));
+                return target;
+            } catch (IOException | ClassNotFoundException e) {
+                throw new ConversionException(e);
+            }
+        }
+
+        @Override
+        public DomainModel convert(LinkedHashMap<String, Object> obj) throws 
ConversionException {
+            // cant convert without target class, so abort
+            throw new ConversionException("Need target class to convert from 
HashMap to DomainModel");
+        }
+    }
+
+    /**
+     * Load this class <code>ModelConverters</code>.
+     */
+    public void loadConverters() {
+        Converters.loadContainedConverters(ModelConverters.class);
+    }
+
+    /**
+     * Method to convert serviceParameter List values to specific DomainModel
+     * @param context         the service context map containing the list to 
convert
+     * @param listName        the key under which the raw List is stored in 
the context
+     * @param domainModelClass the target DomainModel class each element 
should be converted to
+     * @return a List of converted DomainModel objects; an empty List if the 
raw list is missing or empty
+     * @throws GeneralException
+     */
+    public static List<DomainModel> 
convertRawListToDomainModelType(Map<String, Object> context, String listName, 
Class<?> domainModelClass)
+            throws GeneralException {
+        List<Object> dataList = 
UtilGenerics.checkCollection(context.get(listName), Object.class);
+
+        return convertRawListToDomainModelType(dataList, domainModelClass);
+    }
+
+    /**
+     * Converts objects in list to specified DomainModel
+     * @param dataList        the List of raw objects to convert
+     * @param domainModelClass the target DomainModel class each element 
should be converted to
+     * @return a List of converted DomainModel objects; an empty List if 
{@code dataList} is empty
+     * @throws GeneralException
+     */
+    public static List<DomainModel> 
convertRawListToDomainModelType(List<Object> dataList, Class<?> 
domainModelClass) throws GeneralException {
+        List<DomainModel> domainModelList = new ArrayList<>();
+        if (UtilValidate.isEmpty(dataList)) {
+            return domainModelList;
+        }
+        for (Object rawData : dataList) {
+            DomainModel domainObject = (DomainModel) 
ObjectType.simpleTypeOrObjectConvert(rawData, domainModelClass.getName(), null, 
null);
+            domainModelList.add(domainObject);
+        }
+        return domainModelList;
+    }
+}
diff --git 
a/framework/base/src/main/java/org/apache/ofbiz/base/model/DomainModel.java 
b/framework/base/src/main/java/org/apache/ofbiz/base/model/DomainModel.java
new file mode 100644
index 0000000000..98ade52d78
--- /dev/null
+++ b/framework/base/src/main/java/org/apache/ofbiz/base/model/DomainModel.java
@@ -0,0 +1,23 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.ofbiz.base.model;
+
+public abstract class DomainModel {
+
+}
diff --git 
a/framework/base/src/main/java/org/apache/ofbiz/base/util/ObjectType.java 
b/framework/base/src/main/java/org/apache/ofbiz/base/util/ObjectType.java
index 01641ce9a4..a63e198f69 100644
--- a/framework/base/src/main/java/org/apache/ofbiz/base/util/ObjectType.java
+++ b/framework/base/src/main/java/org/apache/ofbiz/base/util/ObjectType.java
@@ -31,6 +31,7 @@ import org.apache.ofbiz.base.conversion.ConversionException;
 import org.apache.ofbiz.base.conversion.Converter;
 import org.apache.ofbiz.base.conversion.Converters;
 import org.apache.ofbiz.base.conversion.LocalizedConverter;
+import org.apache.ofbiz.base.conversion.ModelConverter;
 import org.apache.ofbiz.base.lang.IsEmpty;
 import org.apache.ofbiz.base.lang.SourceMonitored;
 import org.w3c.dom.Node;
@@ -349,6 +350,15 @@ public class ObjectType {
                     Debug.logWarning(e, "Exception thrown while converting 
type: ", MODULE);
                     throw new GeneralException(e.getMessage(), e);
                 }
+            } else if (converter instanceof ModelConverter) {
+                @SuppressWarnings("rawtypes")
+                ModelConverter<Object, Object> modelConverter = 
(ModelConverter) converter;
+                try {
+                    return modelConverter.convert(obj, targetClass.getName());
+                } catch (ConversionException e) {
+                    Debug.logWarning(e, "Exception thrown while converting 
type: ", MODULE);
+                    throw new GeneralException(e.getMessage(), e);
+                }
             }
 
             try {
diff --git 
a/framework/base/src/test/java/org/apache/ofbiz/base/conversion/TestDomainModelConversion.java
 
b/framework/base/src/test/java/org/apache/ofbiz/base/conversion/TestDomainModelConversion.java
new file mode 100644
index 0000000000..a6bcd4c319
--- /dev/null
+++ 
b/framework/base/src/test/java/org/apache/ofbiz/base/conversion/TestDomainModelConversion.java
@@ -0,0 +1,234 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.ofbiz.base.conversion;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.ofbiz.base.conversion.ModelConverters.HashMapToDomainModel;
+import org.apache.ofbiz.base.model.DomainModel;
+import org.apache.ofbiz.base.util.GeneralException;
+import org.junit.jupiter.api.Test;
+
+class TestDomainModelConversion {
+
+
+    public static class TestPerson extends DomainModel {
+        private String name;
+        private Integer age;
+        private List<TestAddress> addresses;
+
+        public String getName() {
+            return name;
+        }
+
+        public void setName(String name) {
+            this.name = name;
+        }
+
+        public Integer getAge() {
+            return age;
+        }
+
+        public void setAge(Integer age) {
+            this.age = age;
+        }
+
+        public List<TestAddress> getAddresses() {
+            return addresses;
+        }
+
+        public void setAddresses(List<TestAddress> addresses) {
+            this.addresses = addresses;
+        }
+    }
+
+    public static class TestAddress extends DomainModel {
+        private String city;
+        private String zip;
+
+        public String getCity() {
+            return city;
+        }
+
+        public void setCity(String city) {
+            this.city = city;
+        }
+
+        public String getZip() {
+            return zip;
+        }
+
+        public void setZip(String zip) {
+            this.zip = zip;
+        }
+    }
+
+    @Test
+    void convertsFlatMapToDomainModel() throws Exception {
+        LinkedHashMap<String, Object> map = new LinkedHashMap<>();
+        map.put("name", "some name");
+        map.put("age", 33);
+
+        HashMapToDomainModel converter = new HashMapToDomainModel();
+        DomainModel result = converter.convert(map, 
TestPerson.class.getName());
+
+        assertTrue(result instanceof TestPerson);
+        TestPerson person = (TestPerson) result;
+        assertEquals("some name", person.getName());
+        assertEquals(33, person.getAge());
+    }
+
+    @Test
+    void convertsNestedListOfDomainModels() throws Exception {
+        LinkedHashMap<String, Object> address = new LinkedHashMap<>();
+        address.put("city", "Berlin");
+        address.put("zip", "10115");
+
+        LinkedHashMap<String, Object> map = new LinkedHashMap<>();
+        map.put("name", "some name");
+        map.put("addresses", List.of(address));
+
+        HashMapToDomainModel converter = new HashMapToDomainModel();
+        TestPerson person = (TestPerson) converter.convert(map, 
TestPerson.class.getName());
+
+        assertNotNull(person.getAddresses());
+        assertEquals(1, person.getAddresses().size());
+        assertEquals("Berlin", person.getAddresses().get(0).getCity());
+    }
+
+    @Test
+    void throwsConversionExceptionForUnknownTargetClass() {
+        HashMapToDomainModel converter = new HashMapToDomainModel();
+        assertThrows(ConversionException.class, () -> converter.convert(new 
LinkedHashMap<>(), "non.existing.Class"));
+    }
+
+    @Test
+    void throwsConversionExceptionForMissingTargetClass() {
+        HashMapToDomainModel converter = new HashMapToDomainModel();
+        assertThrows(ConversionException.class, () -> converter.convert(new 
LinkedHashMap<>()));
+    }
+
+    @Test
+    void throwsConversionExceptionOnUnknownFields() throws Exception {
+        LinkedHashMap<String, Object> map = new LinkedHashMap<>();
+        map.put("name", "some name");
+        map.put("age", 42);
+        map.put("unknownField", "unknownValue");
+
+        HashMapToDomainModel converter = new HashMapToDomainModel();
+        assertThrows(ConversionException.class, () -> converter.convert(map, 
TestPerson.class.getName()));
+    }
+
+    @Test
+    void nullValuesInMapMapToNullFields() throws Exception {
+        LinkedHashMap<String, Object> map = new LinkedHashMap<>();
+        map.put("name", null);
+        map.put("age", 30);
+
+        HashMapToDomainModel converter = new HashMapToDomainModel();
+        TestPerson person = (TestPerson) converter.convert(map, 
TestPerson.class.getName());
+
+        assertNull(person.getName());
+        assertEquals(30, person.getAge());
+    }
+
+    // tests for convertRawKustToDomainModel
+
+    private LinkedHashMap<String, Object> personMap(String name, int age) {
+        LinkedHashMap<String, Object> map = new LinkedHashMap<>();
+        map.put("name", name);
+        map.put("age", age);
+        return map;
+    }
+
+    @Test
+    void convertsListOfMapsToListOfDomainModels() throws Exception {
+        List<Object> raw = List.of(personMap("Some Name", 42), 
personMap("Other Name", 30));
+
+        List<DomainModel> result = 
ModelConverters.convertRawListToDomainModelType(raw, TestPerson.class);
+
+        assertEquals(2, result.size());
+        assertEquals("Some Name", ((TestPerson) result.get(0)).getName());
+        assertEquals("Other Name", ((TestPerson) result.get(1)).getName());
+    }
+
+    @Test
+    void returnsEmptyListForNullList() throws Exception {
+        List<DomainModel> result = 
ModelConverters.convertRawListToDomainModelType((List<Object>) null, 
TestPerson.class);
+        assertNotNull(result);
+        assertTrue(result.isEmpty());
+    }
+
+    @Test
+    void returnsEmptyListForEmptyList() throws Exception {
+        List<DomainModel> result = 
ModelConverters.convertRawListToDomainModelType(List.of(), TestPerson.class);
+        assertTrue(result.isEmpty());
+    }
+
+    @Test
+    void throwsGeneralExceptionWhenElementIsNotConvertible() {
+        List<Object> raw = List.of("not a map");
+
+        assertThrows(GeneralException.class, () -> 
ModelConverters.convertRawListToDomainModelType(raw, TestPerson.class));
+    }
+
+    @Test
+    void contextOverloadExtractsNamedListAndConverts() throws Exception {
+        Map<String, Object> context = Map.of("people", List.of(personMap("Some 
Name", 42)));
+
+        List<DomainModel> result = 
ModelConverters.convertRawListToDomainModelType(context, "people", 
TestPerson.class);
+
+        assertEquals(1, result.size());
+        assertEquals("Some Name", ((TestPerson) result.get(0)).getName());
+    }
+
+    @Test
+    void contextOverloadReturnsEmptyListWhenKeyMissing() throws Exception {
+        Map<String, Object> context = Map.of("otherKey", "irrelevant");
+
+        List<DomainModel> result = 
ModelConverters.convertRawListToDomainModelType(context, "people", 
TestPerson.class);
+
+        assertTrue(result.isEmpty());
+    }
+
+    @Test
+    void contextOverloadReturnsEmptyListWhenValueForKeyIsNull() throws 
Exception {
+        Map<String, Object> context = new LinkedHashMap<>();
+        context.put("people", null);
+
+        List<DomainModel> result = 
ModelConverters.convertRawListToDomainModelType(context, "people", 
TestPerson.class);
+
+        assertTrue(result.isEmpty());
+    }
+
+    @Test
+    void contextOverloadThrowsIfNamedValueIsNotAList() {
+        Map<String, Object> context = Map.of("people", "not a list at all");
+
+        assertThrows(ClassCastException.class, () -> 
ModelConverters.convertRawListToDomainModelType(context, "people", 
TestPerson.class));
+    }
+}
diff --git 
a/framework/entity/src/main/resources/META-INF/services/org.apache.ofbiz.base.conversion.ConverterLoader
 
b/framework/entity/src/main/resources/META-INF/services/org.apache.ofbiz.base.conversion.ConverterLoader
index 363ccbeb29..1c367b9812 100644
--- 
a/framework/entity/src/main/resources/META-INF/services/org.apache.ofbiz.base.conversion.ConverterLoader
+++ 
b/framework/entity/src/main/resources/META-INF/services/org.apache.ofbiz.base.conversion.ConverterLoader
@@ -23,3 +23,4 @@ org.apache.ofbiz.base.conversion.JSONConverters
 org.apache.ofbiz.base.conversion.MiscConverters
 org.apache.ofbiz.base.conversion.NetConverters
 org.apache.ofbiz.base.conversion.NumberConverters
+org.apache.ofbiz.base.conversion.ModelConverters
diff --git a/framework/rest-api/dtd/rest-api.xsd 
b/framework/rest-api/dtd/rest-api.xsd
index 43273abb34..44c28948a3 100644
--- a/framework/rest-api/dtd/rest-api.xsd
+++ b/framework/rest-api/dtd/rest-api.xsd
@@ -23,6 +23,7 @@ under the License.
         <xs:complexType>
             <xs:sequence>
                 <xs:element minOccurs="0" maxOccurs="unbounded" 
ref="resource"/>
+                <xs:element minOccurs="0" maxOccurs="unbounded" ref="mapping"/>
             </xs:sequence>
             <xs:attribute name="name" type="xs:string" use="required"/>
             <xs:attribute name="path" type="xs:string" use="required"/>
@@ -87,4 +88,10 @@ under the License.
             <xs:attribute name="name" type="xs:string" use="required"/>
         </xs:complexType>
     </xs:element>
+    <xs:element name="mapping">
+        <xs:complexType>
+            <xs:attribute name="name" type="xs:string" />
+            <xs:attribute name="className" type="xs:string" />
+        </xs:complexType>
+    </xs:element>
 </xs:schema>
diff --git 
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelApi.java 
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelApi.java
index 985191a8c3..b3341c26be 100644
--- 
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelApi.java
+++ 
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelApi.java
@@ -24,6 +24,7 @@ import java.util.List;
 public class ModelApi {
 
     private List<ModelResource> resources;
+    private List<ModelMapping> mappings;
     private String name;
     private String path;
     private String displayName;
@@ -38,7 +39,7 @@ public class ModelApi {
      */
     public List<ModelResource> getResources() {
         if (resources == null) {
-            resources = new ArrayList<ModelResource>();
+            resources = new ArrayList<>();
         }
         return this.resources;
     }
@@ -58,6 +59,30 @@ public class ModelApi {
         return this;
     }
 
+    /**
+     * Returns the mappings
+     *
+     * @return mappings List
+     */
+    public List<ModelMapping> getMappings() {
+        if (mappings == null) {
+            mappings = new ArrayList<>();
+        }
+        return this.mappings;
+    }
+
+    /**
+     * Adds a mapping List
+     *
+     * @param mapping the {@link ModelMapping} to add
+     */
+    public ModelApi addMapping(ModelMapping mapping) {
+        if (this.mappings == null) {
+            this.mappings = new ArrayList<>();
+        }
+        this.mappings.add(mapping);
+        return this;
+    }
     /**
      * Returns the name.
      *
diff --git 
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelApiReader.java
 
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelApiReader.java
index e6110c8cf1..e8987b4962 100644
--- 
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelApiReader.java
+++ 
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelApiReader.java
@@ -65,9 +65,18 @@ public final class ModelApiReader {
         for (Element resourceEle : UtilXml.childElementList(docElement, 
"resource")) {
             createModelResource(resourceEle, api);
         }
+        for (Element mappingEle : UtilXml.childElementList(docElement, 
"mapping")) {
+            createModelMapping(mappingEle, api);
+        }
         return api;
     }
 
+    private static void createModelMapping(Element mappingEle, ModelApi 
modelApi) {
+        ModelMapping mapping = new 
ModelMapping().name(UtilXml.checkEmpty(mappingEle.getAttribute("name")).intern()).className(UtilXml.checkEmpty(
+                mappingEle.getAttribute("className")).intern());
+        modelApi.addMapping(mapping);
+    }
+
     private static void createModelResource(Element resourceEle, ModelApi api) 
{
         ModelResource resource = buildResource(resourceEle);
         createOperations(resourceEle, resource);
diff --git 
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelMapping.java
 
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelMapping.java
new file mode 100644
index 0000000000..782409cb76
--- /dev/null
+++ 
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelMapping.java
@@ -0,0 +1,71 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.ofbiz.ws.rs.model;
+
+public class ModelMapping {
+
+    private String name;
+    private String className;
+
+    /**
+     * @return the name
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * @param name
+     * @return
+     */
+    public ModelMapping name(String name) {
+        this.name = name;
+        return this;
+    }
+
+    /**
+     * @param name the name to set
+     */
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    /**
+     * @return the className
+     */
+    public String getClassName() {
+        return className;
+    }
+
+    /**
+     * @param className
+     * @return
+     */
+    public ModelMapping className(String className) {
+        this.className = className;
+        return this;
+    }
+
+    /**
+     * @param className the className to set
+     */
+    public void setClassName(String className) {
+        this.className = className;
+    }
+}
diff --git 
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelQueryParam.java
 
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelQueryParam.java
new file mode 100644
index 0000000000..9ae8d8be49
--- /dev/null
+++ 
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/model/ModelQueryParam.java
@@ -0,0 +1,80 @@
+package org.apache.ofbiz.ws.rs.model;
+
+
+public class ModelQueryParam {
+
+    private String name;
+    private String description;
+    private String type;
+
+    /**
+     * @return the name
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * @param name the name to set
+     */
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    /**
+     * @param name
+     * @return
+     */
+    public ModelQueryParam name(String name) {
+        this.name = name;
+        return this;
+    }
+
+    /**
+     * @return the description
+     */
+    public String getDescription() {
+        return description;
+    }
+
+
+    /**
+     * @param description the description to set
+     */
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    /**
+     * @param description
+     * @return
+     */
+    public ModelQueryParam description(String description) {
+        this.description = description;
+        return this;
+    }
+
+    /**
+     * @return the type
+     */
+    public String getType() {
+        return type != null ? type : "string";
+    }
+
+    /**
+     * @param type the type to set
+     */
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    /**
+     * @param type
+     * @return
+     */
+    public ModelQueryParam type(String type) {
+        this.type = type;
+        return this;
+    }
+
+}
diff --git 
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/openapi/OFBizOpenApiReader.java
 
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/openapi/OFBizOpenApiReader.java
index 4f262cc1f5..48a934daf3 100644
--- 
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/openapi/OFBizOpenApiReader.java
+++ 
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/openapi/OFBizOpenApiReader.java
@@ -24,11 +24,6 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
-import jakarta.servlet.ServletContext;
-import jakarta.ws.rs.HttpMethod;
-import jakarta.ws.rs.core.HttpHeaders;
-import jakarta.ws.rs.core.Response;
-
 import org.apache.ofbiz.base.util.Debug;
 import org.apache.ofbiz.service.DispatchContext;
 import org.apache.ofbiz.service.GenericServiceException;
@@ -39,6 +34,7 @@ import org.apache.ofbiz.webapp.WebAppUtil;
 import org.apache.ofbiz.ws.rs.core.OFBizApiConfig;
 import org.apache.ofbiz.ws.rs.listener.ApiContextListener;
 import org.apache.ofbiz.ws.rs.model.ModelApi;
+import org.apache.ofbiz.ws.rs.model.ModelMapping;
 import org.apache.ofbiz.ws.rs.model.ModelOperation;
 import org.apache.ofbiz.ws.rs.model.ModelResource;
 import org.apache.ofbiz.ws.rs.util.OpenApiUtil;
@@ -65,6 +61,10 @@ import io.swagger.v3.oas.models.responses.ApiResponse;
 import io.swagger.v3.oas.models.responses.ApiResponses;
 import io.swagger.v3.oas.models.security.SecurityRequirement;
 import io.swagger.v3.oas.models.tags.Tag;
+import jakarta.servlet.ServletContext;
+import jakarta.ws.rs.HttpMethod;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.Response;
 
 public final class OFBizOpenApiReader extends Reader implements OpenApiReader {
     private static final String MODULE = OFBizOpenApiReader.class.getName();
@@ -108,6 +108,10 @@ public final class OFBizOpenApiReader extends Reader 
implements OpenApiReader {
         apis.forEach((k, api) -> {
             if (!api.isPublish()) return;
 
+            List<ModelMapping> mappings = api.getMappings();
+            mappings.forEach(modelMapping -> {
+                OpenApiUtil.getListTypes().put(modelMapping.getName(), 
modelMapping.getClassName());
+            });
             List<String> baseSegments = new ArrayList<>();
             baseSegments.add(api.getPath());
 
diff --git 
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/util/OpenApiUtil.java 
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/util/OpenApiUtil.java
index 52d60310a8..694ad507ee 100644
--- 
a/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/util/OpenApiUtil.java
+++ 
b/framework/rest-api/src/main/java/org/apache/ofbiz/ws/rs/util/OpenApiUtil.java
@@ -18,7 +18,12 @@
  
*******************************************************************************/
 package org.apache.ofbiz.ws.rs.util;
 
+import java.lang.reflect.Field;
 import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -59,6 +64,7 @@ public final class OpenApiUtil {
 
     }
 
+    private static final Map<String, String> LIST_TYPE_MAP = new HashMap<>();
     private static final Map<String, String> CLASS_ALIAS = new HashMap<>();
     private static final Map<String, Class<?>> JAVA_OPEN_API_MAP = new 
HashMap<>();
     private static final Map<String, String> FIELD_TYPE_MAP = new 
HashMap<String, String>();
@@ -282,6 +288,9 @@ public final class OpenApiUtil {
      * @return the corresponding OpenAPI schema class, or {@code null} if no 
mapping exists
      */
     public static Class<?> getOpenApiTypeForAttributeType(String 
attributeType) {
+        if (isTypeDomainModel(attributeType)) {
+            return MapSchema.class;
+        }
         return JAVA_OPEN_API_MAP.get(CLASS_ALIAS.get(attributeType));
     }
 
@@ -359,7 +368,11 @@ public final class OpenApiUtil {
         Delegator delegator = 
WebAppUtil.getDelegator(ApiContextListener.getApplicationCntx());
         if (schema instanceof ArraySchema) {
             ArraySchema arrSch = (ArraySchema) schema;
-            arrSch.setItems(children.size() > 0 ? getAttributeSchema(service, 
children.get(0)) : new StringSchema());
+            if (LIST_TYPE_MAP.containsKey(param.getName())) {
+                
arrSch.setItems(getSchemaForModel(LIST_TYPE_MAP.get(param.getName())));
+            } else {
+                arrSch.setItems(children.size() > 0 ? 
getAttributeSchema(service, children.get(0)) : new StringSchema());
+            }
         } else if (schema instanceof MapSchema) {
             if (isTypeGenericEntityOrGenericValue(param.getType())) {
                 if (UtilValidate.isEmpty(param.getEntityName())) {
@@ -370,6 +383,8 @@ public final class OpenApiUtil {
                     return null;
                 }
                 schema = 
getSchemaForEntity(delegator.getModelEntity(param.getEntityName()));
+            } else if (isTypeDomainModel(param.getType())) {
+                schema = getSchemaForModel(param.getType());
             } else if (UtilValidate.isEmpty(param.getChildren())) {
                 Debug.logWarning(
                         "Attribute '" + param.getName() + "' ignored as it is 
declared as '" + param.getType() + "' but does not have "
@@ -427,6 +442,81 @@ public final class OpenApiUtil {
         return 
type.matches("org.apache.ofbiz.entity.GenericValue|GenericValue|org.apache.ofbiz.entity.GenericEntity|GenericEntity");
     }
 
+    private static boolean isTypeDomainModel(String className) {
+        if (className == null || CLASS_ALIAS.containsKey(className)) {
+            return false;
+        }
+        try {
+            Class<?> modelClass = Class.forName(className);
+            Class<?> domainModelClass = 
Class.forName("org.apache.ofbiz.base.model.DomainModel");
+            return domainModelClass.isAssignableFrom(modelClass);
+        } catch (ClassNotFoundException e) {
+            Debug.logInfo("Class not found while checking DomainModel type: " 
+ className, MODULE);
+        }
+        return false;
+    }
+
+    @SuppressWarnings("unchecked")
+    private static Schema<?> getSchemaForModel(String className) {
+        Schema<?> dataSchema = new Schema<>();
+        dataSchema.setType("object");
+        try {
+            Class<?> modelClass = Class.forName(className);
+            List<Field> fields = getFields(modelClass);
+            for (Field field : fields) {
+                String fieldNm = field.getName();
+                Class<?> fieldType = field.getType();
+                Schema<?> schema = null;
+                Class<?> schemaClass = 
getOpenApiTypeForAttributeType(fieldType.getName());
+                if (schemaClass == null) {
+                    continue;
+                }
+                try {
+                    schema = (Schema<?>) schemaClass.newInstance();
+                    if (schema instanceof ArraySchema) {
+                        ParameterizedType genericType = (ParameterizedType) 
field.getGenericType();
+                        Class<? extends Type> genericClass = (Class<? extends 
Type>) genericType.getActualTypeArguments()[0];
+                        ArraySchema arrSch = (ArraySchema) schema;
+                        Class<?> listSchemaClass = 
getOpenApiTypeForAttributeType(genericClass.getName());
+                        Schema<?> listSchema = null;
+                        if (listSchemaClass == null || 
isTypeDomainModel(genericClass.getName())) {
+                            
arrSch.setItems(getSchemaForModel(genericClass.getName()));
+                        } else {
+                            listSchema = (Schema<?>) 
listSchemaClass.newInstance();
+                            arrSch.setItems(listSchema);
+                        }
+                        dataSchema.addProperties(fieldNm, arrSch);
+                    } else if (schema instanceof MapSchema) {
+                        if (isTypeDomainModel(fieldType.getName())) {
+                            schema = getSchemaForModel(fieldType.getName());
+                            dataSchema.addProperties(fieldNm, schema);
+                        }
+                    } else {
+                        dataSchema.addProperties(fieldNm, 
schema.description(fieldNm));
+                    }
+                } catch (InstantiationException | IllegalAccessException e) {
+                    e.printStackTrace();
+                }
+            }
+        } catch (ClassNotFoundException e1) {
+            e1.printStackTrace();
+        }
+        return dataSchema;
+    }
+
+    private static <T> List<Field> getFields(Class<?> clazz) {
+        List<Field> fields = new ArrayList<>();
+        while (clazz != Object.class) {
+            fields.addAll(0, Arrays.asList(clazz.getDeclaredFields()));
+            clazz = clazz.getSuperclass();
+        }
+        return fields;
+    }
+
+    public static Map<String, String> getListTypes() {
+        return LIST_TYPE_MAP;
+    }
+
     private static Schema<?> getSchemaForEntity(ModelEntity entity) {
         Schema<?> dataSchema = new Schema<>();
         dataSchema.setType("object");
diff --git 
a/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/util/OpenApiUtilTest.java
 
b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/util/OpenApiUtilTest.java
new file mode 100644
index 0000000000..0efbfe30ec
--- /dev/null
+++ 
b/framework/rest-api/src/test/java/org/apache/ofbiz/ws/rs/util/OpenApiUtilTest.java
@@ -0,0 +1,75 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.ofbiz.ws.rs.util;
+
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.apache.ofbiz.base.model.DomainModel;
+
+import org.junit.jupiter.api.Test;
+
+import io.swagger.v3.oas.models.media.MapSchema;
+import io.swagger.v3.oas.models.media.StringSchema;
+
+class OpenApiUtilTest {
+
+    public static class TestModel extends DomainModel {
+        private String name;
+
+        public String getName() {
+            return name;
+        }
+
+        public void setName(String name) {
+            this.name = name;
+        }
+    }
+
+
+    @Test
+    void domainModelSubclassResolvesToMapSchema() {
+        Class<?> result = 
OpenApiUtil.getOpenApiTypeForAttributeType(TestModel.class.getName());
+        assertEquals(MapSchema.class, result);
+    }
+
+    @Test
+    void classInAliasMapIsNotTreatedAsDomainModel() {
+        Class<?> result = 
OpenApiUtil.getOpenApiTypeForAttributeType("java.sql.Date");
+        assertEquals(StringSchema.class, result);
+    }
+
+    @Test
+    void nullClassNameResolvesToNullWithoutThrowing() {
+        Class<?> result = OpenApiUtil.getOpenApiTypeForAttributeType(null);
+        assertNull(result);
+    }
+
+    @Test
+    void nonexistentClassNameResolvesToNullWithoutThrowing() {
+        Class<?> result = 
OpenApiUtil.getOpenApiTypeForAttributeType("non.existing.Class");
+        assertNull(result);
+    }
+
+    @Test
+    void unmappedUnrelatedClassResolvesToNull() {
+        Class<?> result = 
OpenApiUtil.getOpenApiTypeForAttributeType(Object.class.getName());
+        assertNull(result);
+    }
+}

Reply via email to