From 64e390d5afc30240c68728a0d673f7a900f49aa4 Mon Sep 17 00:00:00 2001 From: Philip Garrett Date: Tue, 23 Dec 2025 00:34:39 -0300 Subject: [PATCH 1/7] Enabling support for additionalProperties --- .../dtoreflectiveassert/AdditionalPropsDto.kt | 6 + .../AdditionalPropsInlineDto.kt | 14 +++ .../AdditionalPropsNoRootDto.kt | 14 +++ .../AdditionalPropsRefDto.kt | 14 +++ .../DtoReflectiveAssertRest.kt | 16 +++ .../static/openapi-dto-reflective-assert.yaml | 66 ++++++++++ .../DtoReflectiveAssertEMTest.kt | 12 ++ .../org/evomaster/core/output/dto/DtoClass.kt | 25 +++- .../evomaster/core/output/dto/DtoWriter.kt | 118 +++++++++++++++--- .../evomaster/core/output/dto/GeneToDto.kt | 2 +- .../core/output/dto/JavaDtoOutput.kt | 42 +++++-- .../evomaster/core/output/dto/JvmDtoOutput.kt | 14 +++ .../core/output/dto/KotlinDtoOutput.kt | 33 ++++- .../evomaster/core/search/gene/ObjectGene.kt | 1 + .../core/output/dto/DtoWriterTest.kt | 11 +- 15 files changed, 351 insertions(+), 37 deletions(-) create mode 100644 core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/AdditionalPropsDto.kt create mode 100644 core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/AdditionalPropsInlineDto.kt create mode 100644 core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/AdditionalPropsNoRootDto.kt create mode 100644 core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/AdditionalPropsRefDto.kt diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/AdditionalPropsDto.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/AdditionalPropsDto.kt new file mode 100644 index 0000000000..aea4f448ed --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/AdditionalPropsDto.kt @@ -0,0 +1,6 @@ +package com.foo.rest.examples.spring.openapi.v3.dtoreflectiveassert + +class AdditionalPropsDto( + val value: String, + val source: String? = null +) diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/AdditionalPropsInlineDto.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/AdditionalPropsInlineDto.kt new file mode 100644 index 0000000000..e96aa49a8e --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/AdditionalPropsInlineDto.kt @@ -0,0 +1,14 @@ +package com.foo.rest.examples.spring.openapi.v3.dtoreflectiveassert + +import com.fasterxml.jackson.annotation.JsonAnySetter + +class AdditionalPropsInlineDto(val stringProp: String) { + + val additional: MutableMap = mutableMapOf() + + @JsonAnySetter + fun addAdditional(key: String, value: AdditionalPropsDto) { + additional[key] = value + } + +} diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/AdditionalPropsNoRootDto.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/AdditionalPropsNoRootDto.kt new file mode 100644 index 0000000000..cfe1596f54 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/AdditionalPropsNoRootDto.kt @@ -0,0 +1,14 @@ +package com.foo.rest.examples.spring.openapi.v3.dtoreflectiveassert + +import com.fasterxml.jackson.annotation.JsonAnySetter + +class AdditionalPropsNoRootDto() { + + val additional: MutableMap = mutableMapOf() + + @JsonAnySetter + fun addAdditional(key: String, value: AdditionalPropsDto) { + additional[key] = value + } + +} diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/AdditionalPropsRefDto.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/AdditionalPropsRefDto.kt new file mode 100644 index 0000000000..b0a1c31146 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/AdditionalPropsRefDto.kt @@ -0,0 +1,14 @@ +package com.foo.rest.examples.spring.openapi.v3.dtoreflectiveassert + +import com.fasterxml.jackson.annotation.JsonAnySetter + +class AdditionalPropsRefDto(val stringProp: String) { + + val additional: MutableMap = mutableMapOf() + + @JsonAnySetter + fun addAdditional(key: String, value: ChildSchemaDto) { + additional[key] = value + } + +} diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertRest.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertRest.kt index 8c41fefab4..fa7d9ff20e 100644 --- a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertRest.kt +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertRest.kt @@ -5,6 +5,7 @@ import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RestController +import javax.validation.Valid @RestController class DtoReflectiveAssertRest { @@ -54,4 +55,19 @@ class DtoReflectiveAssertRest { return ResponseEntity.ok("OK") } + @PostMapping(path = ["/additional-properties-inline"], consumes = [MediaType.APPLICATION_JSON_VALUE]) + open fun additionalPropertiesInline(@RequestBody body: AdditionalPropsInlineDto) : ResponseEntity{ + return ResponseEntity.ok("OK") + } + + @PostMapping(path = ["/additional-properties-ref"], consumes = [MediaType.APPLICATION_JSON_VALUE]) + open fun additionalPropertiesRef(@RequestBody body: AdditionalPropsRefDto) : ResponseEntity{ + return ResponseEntity.ok("OK") + } + +// @PostMapping(path = ["/additional-properties-no-root"], consumes = [MediaType.APPLICATION_JSON_VALUE]) +// open fun additionalPropertiesNoRoot(@RequestBody body: AdditionalPropsNoRootDto) : ResponseEntity{ +// return ResponseEntity.ok("OK") +// } + } diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/resources/static/openapi-dto-reflective-assert.yaml b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/resources/static/openapi-dto-reflective-assert.yaml index f2367d8142..39ec979184 100644 --- a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/resources/static/openapi-dto-reflective-assert.yaml +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/resources/static/openapi-dto-reflective-assert.yaml @@ -190,6 +190,72 @@ paths: responses: '200': description: OK + /additional-properties-inline: + post: + summary: Update inline additionalProperties + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + stringProp: + type: string + additionalProperties: + type: object + required: + - value + properties: + value: + type: string + source: + type: string + required: + - stringProp + responses: + '200': + description: OK + /additional-properties-ref: + post: + summary: Update ref additionalProperties + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + stringProp: + type: string + additionalProperties: + $ref: '#/components/schemas/ChildSchema' + required: + - stringProp + responses: + '204': + description: Attributes updated +# /additional-properties-no-root: +# post: +# summary: Update ref additionalProperties +# requestBody: +# required: true +# content: +# application/json: +# schema: +# type: object +# additionalProperties: +# type: object +# required: +# - value +# properties: +# value: +# type: string +# source: +# type: string +# responses: +# '204': +# description: Attributes updated components: schemas: diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertEMTest.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertEMTest.kt index 5ffa8d1f97..eddcfd684c 100644 --- a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertEMTest.kt +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertEMTest.kt @@ -45,6 +45,8 @@ class DtoReflectiveAssertEMTest: SpringTestBase() { assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/items-components", "OK") assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/enum-type", "OK") assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/enum-examples", "OK") + assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/additional-properties-inline", "OK") + assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/additional-properties-ref", "OK") } assertPrimitiveTypeDtoCreated() @@ -55,6 +57,7 @@ class DtoReflectiveAssertEMTest: SpringTestBase() { assertOneOfDtoCreated() assertEnumTypeDtoCreated() assertEnumExampleDtoCreated() +// assertAdditionalPropertiesDtoCreated() } private fun assertPrimitiveTypeDtoCreated() { @@ -127,6 +130,15 @@ class DtoReflectiveAssertEMTest: SpringTestBase() { assertProperty(klass, instance, "listValue", mutableListOf("s1", "s2", "s3")) } + private fun assertAdditionalPropertiesDtoCreated() { + val (klass, instance) = initDtoClass("EnumExample") + assertProperty(klass, instance, "textValue", "A text value") + assertProperty(klass, instance, "flagValue", true) + assertProperty(klass, instance, "countValue", 33) + assertProperty(klass, instance, "scoreValue", 3.14f) + assertProperty(klass, instance, "listValue", mutableListOf("s1", "s2", "s3")) + } + private fun initDtoClass(name: String): Pair, Any> { val className = ClassName("org.foo.dto.$name") val klass = loadClass(className).kotlin diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/DtoClass.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/DtoClass.kt index 66e380ee89..bfab4ea6dc 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/DtoClass.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/DtoClass.kt @@ -2,10 +2,29 @@ package org.evomaster.core.output.dto class DtoClass( val name: String, - val fields: MutableList = mutableListOf()) { +// val fields: MutableList = mutableListOf(), + val fieldsMap: MutableMap = mutableMapOf(), + var hasAdditionalProperties: Boolean = false, +// var additionalProperties: DtoField? = null +) { - fun addField(field: DtoField) { - if (field !in fields) fields.add(field) +// fun addField(field: DtoField) { +// if (field !in fields) fields.add(field) +// } + +// fun hasField(field: DtoField): Boolean { +// return fieldsMap.containsKey(field.name) +// } + + lateinit var additionalPropertiesDtoName: String + + fun addMapField(fieldName: String, field: DtoField) { + if (!fieldsMap.containsKey(fieldName)) { + fieldsMap[fieldName] = field + } } + + + } diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt index f254eb8762..f69b48dd12 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt @@ -12,6 +12,7 @@ import org.evomaster.core.search.gene.Gene import org.evomaster.core.search.gene.ObjectGene import org.evomaster.core.search.gene.collection.ArrayGene import org.evomaster.core.search.gene.collection.EnumGene +import org.evomaster.core.search.gene.collection.PairGene import org.evomaster.core.search.gene.datetime.DateGene import org.evomaster.core.search.gene.datetime.DateTimeGene import org.evomaster.core.search.gene.datetime.TimeGene @@ -19,10 +20,13 @@ import org.evomaster.core.search.gene.numeric.DoubleGene import org.evomaster.core.search.gene.numeric.FloatGene import org.evomaster.core.search.gene.numeric.IntegerGene import org.evomaster.core.search.gene.numeric.LongGene +import org.evomaster.core.search.gene.placeholder.CycleObjectGene import org.evomaster.core.search.gene.regex.RegexGene import org.evomaster.core.search.gene.string.Base64StringGene import org.evomaster.core.search.gene.string.StringGene +import org.evomaster.core.search.gene.utils.GeneUtils.isInactiveOptionalGene import org.evomaster.core.search.gene.wrapper.ChoiceGene +import org.evomaster.core.search.gene.wrapper.OptionalGene import org.evomaster.core.utils.StringUtils import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -96,23 +100,55 @@ class DtoWriter( private fun calculateDtoFromChoice(gene: ChoiceGene<*>, actionName: String) { if (hasObjectOrArrayGene(gene)) { val dtoName = TestWriterUtils.safeVariableName(actionName) - if (!dtoCollector.contains(dtoName)) { - val dtoClass = DtoClass(dtoName) - val children = gene.getViewOfChildren() - // merge options into a single DTO - children.forEach { childGene -> - when (childGene) { - is ObjectGene -> populateDtoClass(dtoClass, childGene) - is ArrayGene<*> -> { - val template = childGene.template - if (template is ObjectGene) { - populateDtoClass(dtoClass, template) - } + + val dtoClass = dtoCollector.computeIfAbsent(dtoName) { DtoClass(dtoName) } + val children = gene.getViewOfChildren() + // merge options into a single DTO + children.forEach { childGene -> + when (childGene) { + is ObjectGene -> populateDtoClass(dtoClass, childGene) + is ArrayGene<*> -> { + val template = childGene.template + if (template is ObjectGene) { + populateDtoClass(dtoClass, template) } } } - dtoCollector.put(dtoName, dtoClass) } + dtoCollector.put(dtoName, dtoClass) +// if (dtoCollector.contains(dtoName)) { +// val dtoClass = dtoCollector[dtoName]!! +// val children = gene.getViewOfChildren() +// // merge options into a single DTO +// children.forEach { childGene -> +// when (childGene) { +// is ObjectGene -> populateDtoClass(dtoClass, childGene) +// is ArrayGene<*> -> { +// val template = childGene.template +// if (template is ObjectGene) { +// populateDtoClass(dtoClass, template) +// } +// } +// } +// } +// dtoCollector.put(dtoName, dtoClass) +// } else { +// val dtoClass = DtoClass(dtoName) +// val children = gene.getViewOfChildren() +// // merge options into a single DTO +// children.forEach { childGene -> +// when (childGene) { +// is ObjectGene -> populateDtoClass(dtoClass, childGene) +// is ArrayGene<*> -> { +// val template = childGene.template +// if (template is ObjectGene) { +// populateDtoClass(dtoClass, template) +// } +// } +// } +// } +// dtoCollector.put(dtoName, dtoClass) +// } } } @@ -141,11 +177,12 @@ class DtoWriter( private fun calculateDtoFromObject(gene: ObjectGene, actionName: String) { // TODO: Determine strategy for objects that are not defined as a component and do not have a name val dtoName = TestWriterUtils.safeVariableName(gene.refType?:actionName) - if (!dtoCollector.contains(dtoName)) { - val dtoClass = DtoClass(dtoName) + val dtoClass = dtoCollector.computeIfAbsent(dtoName) { DtoClass(dtoName) } +// if (!dtoCollector.contains(dtoName)) { +// val dtoClass = DtoClass(dtoName) populateDtoClass(dtoClass, gene) dtoCollector.put(dtoName, dtoClass) - } +// } } private fun calculateDtoFromArray(gene: ArrayGene<*>, actionName: String) { @@ -160,12 +197,18 @@ class DtoWriter( } private fun populateDtoClass(dtoClass: DtoClass, gene: ObjectGene) { - gene.fixedFields.forEach { field -> + val includedFields = gene.fixedFields.filter { + it !is CycleObjectGene && (it !is OptionalGene || (it.isActive && it.gene !is CycleObjectGene)) + } .filter { it.isPrintable() } + + includedFields.forEach { field -> try { val wrappedGene = field.getLeafGene() val dtoField = getDtoField(field.name, wrappedGene) - dtoClass.addField(dtoField) - if (wrappedGene is ObjectGene && !dtoCollector.contains(dtoField.type)) { +// dtoClass.addField(dtoField) + dtoClass.addMapField(field.name, dtoField) +// if (wrappedGene is ObjectGene && !dtoCollector.contains(dtoField.type)) { + if (wrappedGene is ObjectGene) { calculateDtoFromObject(wrappedGene, dtoField.type) } if (wrappedGene is ArrayGene<*> && wrappedGene.template is ObjectGene) { @@ -179,6 +222,43 @@ class DtoWriter( assert(false) } } + if (!gene.isFixed) { + val additionalFields = gene.additionalFields!!.filter { + it.isPrintable() && !isInactiveOptionalGene(it) + } + if (additionalFields.isNotEmpty()) { +// val apType = +// if (outputFormat.isJava()) "Map" else "MutableMap" +// val dtoField = DtoField("additionalProperties", apType) +// dtoClass.additionalProperties = dtoField + dtoClass.hasAdditionalProperties = true + additionalFields.forEach { field -> + try { + val wrappedGene = (field as PairGene).second.getLeafGene() +// val dtoField = getDtoField(field.first.value, wrappedGene) +// dtoClass.addField(dtoField) +// if (wrappedGene is ObjectGene && !dtoCollector.contains(additionalPropertiesDtoName)) { + if (wrappedGene is ObjectGene) { + val additionalPropertiesDtoName = wrappedGene.refType?:"${dtoClass.name}_ap" + dtoClass.additionalPropertiesDtoName = additionalPropertiesDtoName + calculateDtoFromObject(wrappedGene, additionalPropertiesDtoName) + } + if (wrappedGene is ArrayGene<*> && wrappedGene.template is ObjectGene) { + val additionalPropertiesDtoName = wrappedGene.template.refType?:"${dtoClass.name}_ap" + dtoClass.additionalPropertiesDtoName = additionalPropertiesDtoName + calculateDtoFromObject(wrappedGene.template, additionalPropertiesDtoName) + } + } catch (ex: Exception) { + log.warn( + "A failure has occurred when collecting DTOs. \n" + + "Exception: ${ex.localizedMessage} \n" + + "At ${ex.stackTrace.joinToString(separator = " \n -> ")}. " + ) + assert(false) + } + } + } + } } private fun getDtoField(fieldName: String, field: Gene?): DtoField { diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt index 91d78d73bd..879b1f480a 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt @@ -85,7 +85,7 @@ class GeneToDto( val result = mutableListOf() result.add(dtoOutput.getNewObjectStatement(dtoName, dtoVarName)) - val includedFields = gene.fields.filter { + val includedFields = gene.fixedFields.filter { it !is CycleObjectGene && (it !is OptionalGene || (it.isActive && it.gene !is CycleObjectGene)) } .filter { it.isPrintable() } diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/JavaDtoOutput.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/JavaDtoOutput.kt index 9e04b9c905..b4b309482e 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/JavaDtoOutput.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/JavaDtoOutput.kt @@ -51,20 +51,30 @@ class JavaDtoOutput: JvmDtoOutput() { } private fun addVariables(lines: Lines, dtoClass: DtoClass) { - dtoClass.fields.forEach { +// dtoClass.fields.forEach { + dtoClass.fieldsMap.forEach { lines.indented { - lines.add("@JsonProperty(\"${it.name}\")") - lines.add("private Optional<${it.type}> ${it.name};") + lines.add("@JsonProperty(\"${it.key}\")") + lines.add("private Optional<${it.value.type}> ${it.key};") + } + lines.addEmpty() + } + if (dtoClass.hasAdditionalProperties) { + lines.indented { + lines.add("@JsonIgnore") + lines.add("private Map additionalProperties = new HashMap<>();") } lines.addEmpty() } } private fun addGettersAndSetters(lines: Lines, dtoClass: DtoClass) { - dtoClass.fields.forEach { - val varName = it.name - val varType = it.type - val capitalizedVarName = StringUtils.capitalization(varName) +// dtoClass.fields.forEach { + dtoClass.fieldsMap.forEach { + val varName = it.key + val varType = it.value.type +// val capitalizedVarName = StringUtils.capitalization(varName) + val capitalizedVarName = capitalizeFirstChar(varName) lines.indented { lines.add("public Optional<${varType}> get${capitalizedVarName}() {") lines.indented { @@ -80,5 +90,23 @@ class JavaDtoOutput: JvmDtoOutput() { } lines.addEmpty() } + if (dtoClass.hasAdditionalProperties) { + lines.indented { + lines.add("@JsonAnyGetter") + lines.add("public Map getAdditionalProperties() {") + lines.indented { + lines.add("return additionalProperties;") + } + lines.add("}") + lines.addEmpty() + lines.add("@JsonAnySetter") + lines.add("public void addAdditionalProperty(String name, ${dtoClass.additionalPropertiesDtoName} value) {") + lines.indented { + lines.add("this.additionalProperties.put(name, value);") + } + lines.add("}") + } + lines.addEmpty() + } } } diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/JvmDtoOutput.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/JvmDtoOutput.kt index dc48eff72c..ec7aa94268 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/JvmDtoOutput.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/JvmDtoOutput.kt @@ -15,9 +15,14 @@ abstract class JvmDtoOutput: DtoOutput { } protected fun addImports(lines: Lines) { + lines.addStatement("import java.util.HashMap") lines.addStatement("import java.util.List") + lines.addStatement("import java.util.Map") lines.addStatement("import java.util.Optional") lines.addEmpty() + lines.addStatement("import com.fasterxml.jackson.annotation.JsonAnyGetter") + lines.addStatement("import com.fasterxml.jackson.annotation.JsonAnySetter") + lines.addStatement("import com.fasterxml.jackson.annotation.JsonIgnore") lines.addStatement("import com.fasterxml.jackson.annotation.JsonInclude") lines.addStatement("import com.fasterxml.jackson.annotation.JsonProperty") lines.addEmpty() @@ -39,4 +44,13 @@ abstract class JvmDtoOutput: DtoOutput { path.toFile().appendText(testFileContent) } + fun capitalizeFirstChar(word: String) : String { + if(word.isEmpty()){ + return word + } + + return word.substring(0, 1).uppercase() + + word.substring(1) + } + } diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/KotlinDtoOutput.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/KotlinDtoOutput.kt index 15dfd9f8c8..8f288128b9 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/KotlinDtoOutput.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/KotlinDtoOutput.kt @@ -34,19 +34,42 @@ class KotlinDtoOutput: JvmDtoOutput() { private fun declareClass(lines: Lines, dtoFilename: String, dtoClass: DtoClass) { lines.add("@JsonInclude(JsonInclude.Include.NON_NULL)") - lines.add("class $dtoFilename(") + lines.add("class $dtoFilename {") addVariables(lines, dtoClass) - lines.add(")") + lines.add("}") } private fun addVariables(lines: Lines, dtoClass: DtoClass) { - dtoClass.fields.forEach { +// dtoClass.fields.forEach { + dtoClass.fieldsMap.forEach { lines.indented { - lines.add("@JsonProperty(\"${it.name}\")") - lines.add("var ${it.name}: ${it.type}? = null,") + lines.add("@JsonProperty(\"${it.key}\")") + lines.add("var ${it.key}: ${it.value.type}? = null") } lines.addEmpty() } + if (dtoClass.hasAdditionalProperties) { + lines.indented { + lines.add("@JsonIgnore") +// lines.add("val additionalProperties: MutableMap = mutableMapOf()") + lines.add("private val additionalProperties: MutableMap = mutableMapOf()") + lines.addEmpty() + lines.add("@JsonAnyGetter") + lines.add("fun getAdditionalProperties(): MutableMap {") + lines.indented { + lines.add("return additionalProperties") + } + lines.add("}") + lines.addEmpty() + lines.add("@JsonAnySetter") + lines.add("fun addAdditionalProperty(name: String, value: ${dtoClass.additionalPropertiesDtoName}) {") + lines.indented { + lines.add("additionalProperties[name] = value") + } + lines.add("}") + lines.addEmpty() + } + } } } diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/ObjectGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/ObjectGene.kt index 693a826647..c109f95261 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/ObjectGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/ObjectGene.kt @@ -333,6 +333,7 @@ class ObjectGene( if (it.isNotEmpty() && includedFields.isNotEmpty()) buffer.append(", ") }.joinTo(buffer, ", ") { +// val sec = it.second "\"${it.first.value}\":${it.second.getValueAsPrintableString(previousGenes, mode, targetFormat)}" } } diff --git a/core/src/test/kotlin/org/evomaster/core/output/dto/DtoWriterTest.kt b/core/src/test/kotlin/org/evomaster/core/output/dto/DtoWriterTest.kt index 65c4776b08..e89a6fa25c 100644 --- a/core/src/test/kotlin/org/evomaster/core/output/dto/DtoWriterTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/output/dto/DtoWriterTest.kt @@ -15,6 +15,7 @@ import org.evomaster.core.search.Solution import org.evomaster.core.search.action.Action import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.hasItem +import org.hamcrest.Matchers.`is` import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertThrows @@ -107,7 +108,8 @@ class DtoWriterTest { assertEquals(collectedDtos.size, 1) val oneOfDto = collectedDtos[collectedDtos.keys.first()] assertNotNull(oneOfDto) - val dtoFields = oneOfDto?.fields?:emptyList() +// val dtoFields = oneOfDto?.fields?:emptyList() + val dtoFields = oneOfDto?.fieldsMap?:emptyMap() assertEquals(dtoFields.size, 2) assertDtoFieldIn(dtoFields, "dog", STRING) assertDtoFieldIn(dtoFields, "cat", STRING) @@ -125,7 +127,8 @@ class DtoWriterTest { assertEquals(collectedDtos.size, 1) val anyOfDto = collectedDtos[collectedDtos.keys.first()] assertNotNull(anyOfDto) - val dtoFields = anyOfDto?.fields?:emptyList() +// val dtoFields = anyOfDto?.fields?:emptyList() + val dtoFields = anyOfDto?.fieldsMap?:emptyMap() assertEquals(dtoFields.size, 2) assertDtoFieldIn(dtoFields, "email", STRING) assertDtoFieldIn(dtoFields, "numbers", "List") @@ -138,6 +141,10 @@ class DtoWriterTest { return actionCluster } + private fun assertDtoFieldIn(dtoFields: Map, targetName: String, targetType: String) { + assertThat(dtoFields[targetName], `is`(DtoField(targetName, targetType))) + } + private fun assertDtoFieldIn(dtoFields: List, targetName: String, targetType: String) { assertThat(dtoFields, hasItem(DtoField(targetName, targetType))) } From 8f1682acd811a5de02e1428c5f5ab0280547bfa2 Mon Sep 17 00:00:00 2001 From: Philip Garrett Date: Mon, 29 Dec 2025 20:10:38 -0300 Subject: [PATCH 2/7] Added e2e for inline and ref additional properties --- .../DtoReflectiveAssertEMTest.kt | 51 +++++++++++++++---- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertEMTest.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertEMTest.kt index eddcfd684c..1114bdddc7 100644 --- a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertEMTest.kt +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertEMTest.kt @@ -10,8 +10,10 @@ import org.junit.jupiter.api.Test import kotlin.reflect.KClass import kotlin.reflect.KMutableProperty1 import kotlin.reflect.full.createInstance +import kotlin.reflect.full.memberFunctions import kotlin.reflect.full.memberProperties import kotlin.reflect.jvm.isAccessible +import kotlin.reflect.jvm.javaMethod class DtoReflectiveAssertEMTest: SpringTestBase() { @@ -57,7 +59,8 @@ class DtoReflectiveAssertEMTest: SpringTestBase() { assertOneOfDtoCreated() assertEnumTypeDtoCreated() assertEnumExampleDtoCreated() -// assertAdditionalPropertiesDtoCreated() + assertAdditionalPropertiesInlineDtoCreated() + assertAdditionalPropertiesRefDtoCreated() } private fun assertPrimitiveTypeDtoCreated() { @@ -77,9 +80,9 @@ class DtoReflectiveAssertEMTest: SpringTestBase() { private fun assertParentAndChildDtosCreated() { val (parentKlass, parentInstance) = initDtoClass("ParentSchema") - val (childKass, childInstance) = initDtoClass("ChildSchema") - assertProperty(childKass, childInstance, "name", "Philip") - assertProperty(childKass, childInstance, "age", 31) + val (childKlass, childInstance) = initDtoClass("ChildSchema") + assertProperty(childKlass, childInstance, "name", "Philip") + assertProperty(childKlass, childInstance, "age", 31) assertProperty(parentKlass, parentInstance, "label", "EM_TEST") assertProperty(parentKlass, parentInstance, "child", childInstance) } @@ -130,13 +133,21 @@ class DtoReflectiveAssertEMTest: SpringTestBase() { assertProperty(klass, instance, "listValue", mutableListOf("s1", "s2", "s3")) } - private fun assertAdditionalPropertiesDtoCreated() { - val (klass, instance) = initDtoClass("EnumExample") - assertProperty(klass, instance, "textValue", "A text value") - assertProperty(klass, instance, "flagValue", true) - assertProperty(klass, instance, "countValue", 33) - assertProperty(klass, instance, "scoreValue", 3.14f) - assertProperty(klass, instance, "listValue", mutableListOf("s1", "s2", "s3")) + private fun assertAdditionalPropertiesInlineDtoCreated() { + val (parentKlass, parentInstance) = initDtoClass("POST__additional_properties_inline") + assertProperty(parentKlass, parentInstance, "stringProp", "A text value") + val (childKlass, childInstance) = initDtoClass("POST__additional_properties_inline_ap") + assertProperty(childKlass, childInstance, "value", "My value") + assertAdditionalPropertiesFunction(parentKlass, parentInstance, "aRandomKey", childInstance) + } + + private fun assertAdditionalPropertiesRefDtoCreated() { + val (parentKlass, parentInstance) = initDtoClass("POST__additional_properties_ref") + assertProperty(parentKlass, parentInstance, "stringProp", "A text value") + val (childKlass, childInstance) = initDtoClass("ChildSchema") + assertProperty(childKlass, childInstance, "name", "Philip") + assertProperty(childKlass, childInstance, "age", 31) + assertAdditionalPropertiesFunction(parentKlass, parentInstance, "anotherRandomKey", childInstance) } private fun initDtoClass(name: String): Pair, Any> { @@ -158,4 +169,22 @@ class DtoReflectiveAssertEMTest: SpringTestBase() { Assertions.assertEquals(propertyValue, property?.get(instance)) } + private fun assertAdditionalPropertiesFunction(klass: KClass, instance: Any, propertyName: String, propertyValue: Any?) { + val setterFunction = klass.memberFunctions.firstOrNull { + it.name == "addAdditionalProperty" + }?.javaMethod + Assertions.assertNotNull(setterFunction) + setterFunction?.isAccessible = true + setterFunction?.invoke(instance, propertyName, propertyValue) + + val getterFunction = klass.memberFunctions.firstOrNull { + it.name == "getAdditionalProperties" + }?.javaMethod + Assertions.assertNotNull(getterFunction) + getterFunction?.isAccessible = true + val map = getterFunction?.invoke(instance) as MutableMap<*, *> + Assertions.assertNotNull(map) + Assertions.assertEquals(propertyValue, map[propertyName]) + } + } From 367306f40132ffc3c6fce932f9da325f2c9fca52 Mon Sep 17 00:00:00 2001 From: Philip Garrett Date: Tue, 10 Feb 2026 21:09:08 -0300 Subject: [PATCH 3/7] Additional properties supported for nested objects --- .../DtoReflectiveAssertRest.kt | 1 - .../evomaster/core/output/dto/DtoOutput.kt | 2 + .../evomaster/core/output/dto/GeneToDto.kt | 65 ++++++++++++++++++- .../core/output/dto/JavaDtoOutput.kt | 9 ++- .../core/output/dto/KotlinDtoOutput.kt | 4 ++ 5 files changed, 77 insertions(+), 4 deletions(-) diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertRest.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertRest.kt index fa7d9ff20e..544817eb67 100644 --- a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertRest.kt +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertRest.kt @@ -5,7 +5,6 @@ import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RestController -import javax.validation.Valid @RestController class DtoReflectiveAssertRest { diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/DtoOutput.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/DtoOutput.kt index 688cb449ee..10bede1aa8 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/DtoOutput.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/DtoOutput.kt @@ -52,4 +52,6 @@ interface DtoOutput { */ fun getAddElementToListStatement(listVarName: String, value: String): String + fun getAddElementToAdditionalPropertiesStatement(additionalPropertiesVarName: String, key: String, value: String): String + } diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt index 879b1f480a..03b8b9aaaf 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt @@ -7,6 +7,7 @@ import org.evomaster.core.search.gene.Gene import org.evomaster.core.search.gene.ObjectGene import org.evomaster.core.search.gene.collection.ArrayGene import org.evomaster.core.search.gene.collection.EnumGene +import org.evomaster.core.search.gene.collection.PairGene import org.evomaster.core.search.gene.datetime.DateGene import org.evomaster.core.search.gene.datetime.DateTimeGene import org.evomaster.core.search.gene.datetime.TimeGene @@ -18,9 +19,12 @@ import org.evomaster.core.search.gene.placeholder.CycleObjectGene import org.evomaster.core.search.gene.regex.RegexGene import org.evomaster.core.search.gene.string.Base64StringGene import org.evomaster.core.search.gene.string.StringGene +import org.evomaster.core.search.gene.utils.GeneUtils.isInactiveOptionalGene import org.evomaster.core.search.gene.wrapper.ChoiceGene import org.evomaster.core.search.gene.wrapper.OptionalGene import org.evomaster.core.utils.StringUtils +import org.slf4j.Logger +import org.slf4j.LoggerFactory /** * Provides a mapping between a Gene and its DTO representation at use. Takes in the [OutputFormat] to delegate @@ -30,6 +34,8 @@ class GeneToDto( val outputFormat: OutputFormat ) { + private val log: Logger = LoggerFactory.getLogger(GeneToDto::class.java) + private var dtoOutput: DtoOutput = if (outputFormat.isJava()) { JavaDtoOutput() } else if (outputFormat.isKotlin()){ @@ -54,7 +60,7 @@ class GeneToDto( TestWriterUtils.safeVariableName(template.refType?:fallback) } else { // TODO handle arrays of basic data types - return getListType(fallback, template, capitalize) + getListType(fallback, template, capitalize) } } is ChoiceGene<*> -> TestWriterUtils.safeVariableName(fallback) @@ -118,6 +124,63 @@ class GeneToDto( } } + if (!gene.isFixed) { + val additionalFields = gene.additionalFields!!.filter { + it.isPrintable() && !isInactiveOptionalGene(it) + } + if (additionalFields.isNotEmpty()) { + var additionalPropertiesCounter = 1 + additionalFields.forEach { field -> + try { + val childCounter = mutableListOf() + childCounter.addAll(counters) + childCounter.add(additionalPropertiesCounter++) + val key = (field as PairGene).first.getLeafGene().getValueAsPrintableString(targetFormat = outputFormat) + val leafGene = (field as PairGene).second.getLeafGene() + val additionalPropertiesVarName = when (leafGene) { + is ObjectGene -> { + val attributeName = leafGene.refType?:"${dtoName}_ap" + val childDtoCall = getDtoCall(leafGene, getDtoName(leafGene, attributeName, true), childCounter, true) + result.addAll(childDtoCall.objectCalls) + childDtoCall.varName + } + is ArrayGene<*> -> { + val attributeName = if (leafGene.template is ObjectGene) { + leafGene.template.refType?:"${dtoName}_ap" + } else { + getListType("", leafGene.template, true) + } + val childDtoCall = getArrayDtoCall(leafGene, getDtoName(leafGene, attributeName, true), childCounter, attributeName, true) + + result.addAll(childDtoCall.objectCalls) + childDtoCall.varName + } + else -> throw IllegalStateException("Additional properties should only be Map related genes") +// else -> { +// val parent = leafGene.parent +// if (leafGene is EnumGene<*> && parent is ChoiceGene<*>) { +// val children = parent.getViewOfChildren() +// val otherChoice = children.find { child -> child != leafGene } +//// result.add(dtoOutput.getSetterStatement(dtoVarName, attributeName, "${leafGene.getValueAsPrintableString(targetFormat = outputFormat)}${getValueSuffix(otherChoice)}")) +// } else { +//// result.add(dtoOutput.getSetterStatement(dtoVarName, attributeName, "${leafGene.getValueAsPrintableString(targetFormat = outputFormat)}${getValueSuffix(leafGene)}")) +// } +// "" +// } + } + result.add(dtoOutput.getAddElementToAdditionalPropertiesStatement(dtoVarName, key, additionalPropertiesVarName)) + } catch (ex: Exception) { + log.warn( + "A failure has occurred when writing DTOs. \n" + + "Exception: ${ex.localizedMessage} \n" + + "At ${ex.stackTrace.joinToString(separator = " \n -> ")}. " + ) + assert(false) + } + } + } + } + return DtoCall(dtoVarName, result) } diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/JavaDtoOutput.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/JavaDtoOutput.kt index b4b309482e..2efeb3a040 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/JavaDtoOutput.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/JavaDtoOutput.kt @@ -24,7 +24,8 @@ class JavaDtoOutput: JvmDtoOutput() { } override fun getSetterStatement(dtoVarName: String, attributeName: String, value: String): String { - return "$dtoVarName.set${StringUtils.capitalization(attributeName)}($value);" +// return "$dtoVarName.set${StringUtils.capitalization(attributeName)}($value);" + return "$dtoVarName.set${capitalizeFirstChar(attributeName)}($value);" } override fun getNewListStatement(listType: String, listVarName: String): String { @@ -35,6 +36,10 @@ class JavaDtoOutput: JvmDtoOutput() { return "$listVarName.add($value);" } + override fun getAddElementToAdditionalPropertiesStatement(additionalPropertiesVarName: String, key: String, value: String): String { + return "$additionalPropertiesVarName.addAdditionalProperty($key, $value);" + } + private fun initClass(lines: Lines, dtoFilename: String) { lines.add("@JsonInclude(JsonInclude.Include.NON_NULL)") lines.add("public class $dtoFilename {") @@ -62,7 +67,7 @@ class JavaDtoOutput: JvmDtoOutput() { if (dtoClass.hasAdditionalProperties) { lines.indented { lines.add("@JsonIgnore") - lines.add("private Map additionalProperties = new HashMap<>();") + lines.add("private Map additionalProperties = new HashMap<>();") } lines.addEmpty() } diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/KotlinDtoOutput.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/KotlinDtoOutput.kt index 8f288128b9..dfe6faf1eb 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/KotlinDtoOutput.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/KotlinDtoOutput.kt @@ -32,6 +32,10 @@ class KotlinDtoOutput: JvmDtoOutput() { return "$listVarName.add($value)" } + override fun getAddElementToAdditionalPropertiesStatement(additionalPropertiesVarName: String, key: String, value: String): String { + return "$additionalPropertiesVarName.addAdditionalProperty($key, $value)" + } + private fun declareClass(lines: Lines, dtoFilename: String, dtoClass: DtoClass) { lines.add("@JsonInclude(JsonInclude.Include.NON_NULL)") lines.add("class $dtoFilename {") From 1fbb52663d332fc16ca7fd901dbf9017aa20d12d Mon Sep 17 00:00:00 2001 From: Philip Garrett Date: Thu, 12 Feb 2026 10:57:05 -0300 Subject: [PATCH 4/7] Support for additionalProperties in object and array tested --- .../DtoReflectiveAssertRest.kt | 8 +- .../static/openapi-dto-reflective-assert.yaml | 43 +++--- .../DtoReflectiveAssertEMTest.kt | 10 ++ .../evomaster/core/output/dto/DtoWriter.kt | 113 ++++++---------- .../evomaster/core/output/dto/GeneToDto.kt | 123 +++++++++++------- .../output/service/HttpWsTestCaseWriter.kt | 3 +- 6 files changed, 153 insertions(+), 147 deletions(-) diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertRest.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertRest.kt index 544817eb67..3b5f06c2ca 100644 --- a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertRest.kt +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertRest.kt @@ -64,9 +64,9 @@ class DtoReflectiveAssertRest { return ResponseEntity.ok("OK") } -// @PostMapping(path = ["/additional-properties-no-root"], consumes = [MediaType.APPLICATION_JSON_VALUE]) -// open fun additionalPropertiesNoRoot(@RequestBody body: AdditionalPropsNoRootDto) : ResponseEntity{ -// return ResponseEntity.ok("OK") -// } + @PostMapping(path = ["/additional-properties-no-root"], consumes = [MediaType.APPLICATION_JSON_VALUE]) + open fun additionalPropertiesNoRoot(@RequestBody body: AdditionalPropsNoRootDto) : ResponseEntity{ + return ResponseEntity.ok("OK") + } } diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/resources/static/openapi-dto-reflective-assert.yaml b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/resources/static/openapi-dto-reflective-assert.yaml index 39ec979184..e975247445 100644 --- a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/resources/static/openapi-dto-reflective-assert.yaml +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/resources/static/openapi-dto-reflective-assert.yaml @@ -235,27 +235,28 @@ paths: responses: '204': description: Attributes updated -# /additional-properties-no-root: -# post: -# summary: Update ref additionalProperties -# requestBody: -# required: true -# content: -# application/json: -# schema: -# type: object -# additionalProperties: -# type: object -# required: -# - value -# properties: -# value: -# type: string -# source: -# type: string -# responses: -# '204': -# description: Attributes updated + /additional-properties-no-root: + post: + summary: Update ref additionalProperties + requestBody: + required: true + content: + application/json: + schema: + type: object + additionalProperties: + type: object + required: + - value + - source + properties: + value: + type: string + source: + type: string + responses: + '204': + description: Attributes updated components: schemas: diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertEMTest.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertEMTest.kt index 1114bdddc7..accd0aeb47 100644 --- a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertEMTest.kt +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertEMTest.kt @@ -49,6 +49,7 @@ class DtoReflectiveAssertEMTest: SpringTestBase() { assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/enum-examples", "OK") assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/additional-properties-inline", "OK") assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/additional-properties-ref", "OK") + assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/additional-properties-no-root", "OK") } assertPrimitiveTypeDtoCreated() @@ -61,6 +62,7 @@ class DtoReflectiveAssertEMTest: SpringTestBase() { assertEnumExampleDtoCreated() assertAdditionalPropertiesInlineDtoCreated() assertAdditionalPropertiesRefDtoCreated() + assertAdditionalPropertiesNoRootDtoCreated() } private fun assertPrimitiveTypeDtoCreated() { @@ -150,6 +152,14 @@ class DtoReflectiveAssertEMTest: SpringTestBase() { assertAdditionalPropertiesFunction(parentKlass, parentInstance, "anotherRandomKey", childInstance) } + private fun assertAdditionalPropertiesNoRootDtoCreated() { + val (parentKlass, parentInstance) = initDtoClass("POST__additional_properties_no_root") + val (childKlass, childInstance) = initDtoClass("POST__additional_properties_no_root_ap") + assertProperty(childKlass, childInstance, "value", "WebFuzzing") + assertProperty(childKlass, childInstance, "source", "Dataset") + assertAdditionalPropertiesFunction(parentKlass, parentInstance, "no_root_key", childInstance) + } + private fun initDtoClass(name: String): Pair, Any> { val className = ClassName("org.foo.dto.$name") val klass = loadClass(className).kotlin diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt index f69b48dd12..03a6a0bf8a 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt @@ -12,6 +12,7 @@ import org.evomaster.core.search.gene.Gene import org.evomaster.core.search.gene.ObjectGene import org.evomaster.core.search.gene.collection.ArrayGene import org.evomaster.core.search.gene.collection.EnumGene +import org.evomaster.core.search.gene.collection.FixedMapGene import org.evomaster.core.search.gene.collection.PairGene import org.evomaster.core.search.gene.datetime.DateGene import org.evomaster.core.search.gene.datetime.DateTimeGene @@ -115,40 +116,7 @@ class DtoWriter( } } } - dtoCollector.put(dtoName, dtoClass) -// if (dtoCollector.contains(dtoName)) { -// val dtoClass = dtoCollector[dtoName]!! -// val children = gene.getViewOfChildren() -// // merge options into a single DTO -// children.forEach { childGene -> -// when (childGene) { -// is ObjectGene -> populateDtoClass(dtoClass, childGene) -// is ArrayGene<*> -> { -// val template = childGene.template -// if (template is ObjectGene) { -// populateDtoClass(dtoClass, template) -// } -// } -// } -// } -// dtoCollector.put(dtoName, dtoClass) -// } else { -// val dtoClass = DtoClass(dtoName) -// val children = gene.getViewOfChildren() -// // merge options into a single DTO -// children.forEach { childGene -> -// when (childGene) { -// is ObjectGene -> populateDtoClass(dtoClass, childGene) -// is ArrayGene<*> -> { -// val template = childGene.template -// if (template is ObjectGene) { -// populateDtoClass(dtoClass, template) -// } -// } -// } -// } -// dtoCollector.put(dtoName, dtoClass) -// } + dtoCollector[dtoName] = dtoClass } } @@ -160,6 +128,7 @@ class DtoWriter( when { gene is ObjectGene -> calculateDtoFromObject(gene, actionName) gene is ArrayGene<*> -> calculateDtoFromArray(gene, actionName) + gene is FixedMapGene<*, *> -> calculateDtoFromFixedMapGene(gene, actionName) isPrimitiveGene(gene) -> return else -> { throw IllegalStateException("Gene $gene is not supported for DTO payloads for action: $actionName") @@ -167,6 +136,16 @@ class DtoWriter( } } + private fun calculateDtoFromFixedMapGene(gene: FixedMapGene<*, *>, actionName: String) { + val dtoName = TestWriterUtils.safeVariableName(actionName) + val dtoClass = dtoCollector.computeIfAbsent(dtoName) { DtoClass(dtoName) } + val additionalProperties = gene.getViewOfChildren()!!.filter { + it.isPrintable() && !isInactiveOptionalGene(it) + } as List> + addAdditionalProperties(dtoClass, additionalProperties) + dtoCollector[dtoName] = dtoClass + } + private fun isPrimitiveGene(gene: Gene): Boolean { return when (gene) { is StringGene, is IntegerGene, is LongGene, is DoubleGene, is FloatGene, is BooleanGene -> true @@ -178,11 +157,8 @@ class DtoWriter( // TODO: Determine strategy for objects that are not defined as a component and do not have a name val dtoName = TestWriterUtils.safeVariableName(gene.refType?:actionName) val dtoClass = dtoCollector.computeIfAbsent(dtoName) { DtoClass(dtoName) } -// if (!dtoCollector.contains(dtoName)) { -// val dtoClass = DtoClass(dtoName) - populateDtoClass(dtoClass, gene) - dtoCollector.put(dtoName, dtoClass) -// } + populateDtoClass(dtoClass, gene) + dtoCollector[dtoName] = dtoClass } private fun calculateDtoFromArray(gene: ArrayGene<*>, actionName: String) { @@ -205,9 +181,7 @@ class DtoWriter( try { val wrappedGene = field.getLeafGene() val dtoField = getDtoField(field.name, wrappedGene) -// dtoClass.addField(dtoField) dtoClass.addMapField(field.name, dtoField) -// if (wrappedGene is ObjectGene && !dtoCollector.contains(dtoField.type)) { if (wrappedGene is ObjectGene) { calculateDtoFromObject(wrappedGene, dtoField.type) } @@ -225,37 +199,34 @@ class DtoWriter( if (!gene.isFixed) { val additionalFields = gene.additionalFields!!.filter { it.isPrintable() && !isInactiveOptionalGene(it) - } - if (additionalFields.isNotEmpty()) { -// val apType = -// if (outputFormat.isJava()) "Map" else "MutableMap" -// val dtoField = DtoField("additionalProperties", apType) -// dtoClass.additionalProperties = dtoField - dtoClass.hasAdditionalProperties = true - additionalFields.forEach { field -> - try { - val wrappedGene = (field as PairGene).second.getLeafGene() -// val dtoField = getDtoField(field.first.value, wrappedGene) -// dtoClass.addField(dtoField) -// if (wrappedGene is ObjectGene && !dtoCollector.contains(additionalPropertiesDtoName)) { - if (wrappedGene is ObjectGene) { - val additionalPropertiesDtoName = wrappedGene.refType?:"${dtoClass.name}_ap" - dtoClass.additionalPropertiesDtoName = additionalPropertiesDtoName - calculateDtoFromObject(wrappedGene, additionalPropertiesDtoName) - } - if (wrappedGene is ArrayGene<*> && wrappedGene.template is ObjectGene) { - val additionalPropertiesDtoName = wrappedGene.template.refType?:"${dtoClass.name}_ap" - dtoClass.additionalPropertiesDtoName = additionalPropertiesDtoName - calculateDtoFromObject(wrappedGene.template, additionalPropertiesDtoName) - } - } catch (ex: Exception) { - log.warn( - "A failure has occurred when collecting DTOs. \n" - + "Exception: ${ex.localizedMessage} \n" - + "At ${ex.stackTrace.joinToString(separator = " \n -> ")}. " - ) - assert(false) + } as List> + addAdditionalProperties(dtoClass, additionalFields) + } + } + + private fun addAdditionalProperties(dtoClass: DtoClass, additionalProperties: List>) { + if (additionalProperties.isNotEmpty()) { + dtoClass.hasAdditionalProperties = true + additionalProperties.forEach { field -> + try { + val wrappedGene = (field as PairGene).second.getLeafGene() + if (wrappedGene is ObjectGene) { + val additionalPropertiesDtoName = wrappedGene.refType?:"${dtoClass.name}_ap" + dtoClass.additionalPropertiesDtoName = additionalPropertiesDtoName + calculateDtoFromObject(wrappedGene, additionalPropertiesDtoName) + } + if (wrappedGene is ArrayGene<*> && wrappedGene.template is ObjectGene) { + val additionalPropertiesDtoName = wrappedGene.template.refType?:"${dtoClass.name}_ap" + dtoClass.additionalPropertiesDtoName = additionalPropertiesDtoName + calculateDtoFromObject(wrappedGene.template, additionalPropertiesDtoName) } + } catch (ex: Exception) { + log.warn( + "A failure has occurred when collecting DTO additional properties. \n" + + "Exception: ${ex.localizedMessage} \n" + + "At ${ex.stackTrace.joinToString(separator = " \n -> ")}. " + ) + assert(false) } } } diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt index 03b8b9aaaf..5cffdf2fc5 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt @@ -7,6 +7,7 @@ import org.evomaster.core.search.gene.Gene import org.evomaster.core.search.gene.ObjectGene import org.evomaster.core.search.gene.collection.ArrayGene import org.evomaster.core.search.gene.collection.EnumGene +import org.evomaster.core.search.gene.collection.FixedMapGene import org.evomaster.core.search.gene.collection.PairGene import org.evomaster.core.search.gene.datetime.DateGene import org.evomaster.core.search.gene.datetime.DateTimeGene @@ -64,6 +65,7 @@ class GeneToDto( } } is ChoiceGene<*> -> TestWriterUtils.safeVariableName(fallback) + is FixedMapGene<*,*> -> TestWriterUtils.safeVariableName(fallback) else -> throw IllegalStateException("Gene $gene is not supported for DTO payloads for action: $fallback") } } @@ -81,6 +83,7 @@ class GeneToDto( is ObjectGene -> getObjectDtoCall(gene, dtoName, counters) is ArrayGene<*> -> getArrayDtoCall(gene, dtoName, counters, null, capitalize) is ChoiceGene<*> -> getDtoCall(gene.activeGene(), dtoName, counters, capitalize) + is FixedMapGene<*,*> -> getFixedMapGeneDtoCall(gene, dtoName, counters) else -> throw RuntimeException("BUG: Gene $gene (with type ${this::class.java.simpleName}) should not be creating DTOs") } } @@ -125,63 +128,83 @@ class GeneToDto( } if (!gene.isFixed) { - val additionalFields = gene.additionalFields!!.filter { + val additionalProperties = gene.additionalFields!!.filter { it.isPrintable() && !isInactiveOptionalGene(it) - } - if (additionalFields.isNotEmpty()) { - var additionalPropertiesCounter = 1 - additionalFields.forEach { field -> - try { - val childCounter = mutableListOf() - childCounter.addAll(counters) - childCounter.add(additionalPropertiesCounter++) - val key = (field as PairGene).first.getLeafGene().getValueAsPrintableString(targetFormat = outputFormat) - val leafGene = (field as PairGene).second.getLeafGene() - val additionalPropertiesVarName = when (leafGene) { - is ObjectGene -> { - val attributeName = leafGene.refType?:"${dtoName}_ap" - val childDtoCall = getDtoCall(leafGene, getDtoName(leafGene, attributeName, true), childCounter, true) - result.addAll(childDtoCall.objectCalls) - childDtoCall.varName - } - is ArrayGene<*> -> { - val attributeName = if (leafGene.template is ObjectGene) { - leafGene.template.refType?:"${dtoName}_ap" - } else { - getListType("", leafGene.template, true) - } - val childDtoCall = getArrayDtoCall(leafGene, getDtoName(leafGene, attributeName, true), childCounter, attributeName, true) - - result.addAll(childDtoCall.objectCalls) - childDtoCall.varName + } as List> + setAdditionalProperties(dtoName, dtoVarName, result, counters, additionalProperties) + } + + return DtoCall(dtoVarName, result) + } + + private fun getFixedMapGeneDtoCall(gene: FixedMapGene<*, *>, dtoName: String, counters: MutableList): DtoCall { + val dtoVarName = "dto_${dtoName}_${counters.joinToString("_")}" + val result = mutableListOf() + result.add(dtoOutput.getNewObjectStatement(dtoName, dtoVarName)) + val additionalProperties = gene.getViewOfChildren()!!.filter { + it.isPrintable() && !isInactiveOptionalGene(it) + } as List> + setAdditionalProperties(dtoName, dtoVarName, result, counters, additionalProperties) + return DtoCall(dtoVarName, result) + } + + private fun setAdditionalProperties(dtoName: String, dtoVarName: String, result: MutableList, counters: MutableList, additionalProperties: List>) { + if (additionalProperties.isNotEmpty()) { + var additionalPropertiesCounter = 1 + additionalProperties.forEach { field -> + try { + val childCounter = mutableListOf() + childCounter.addAll(counters) + childCounter.add(additionalPropertiesCounter++) + val key = (field as PairGene).first.getLeafGene() + .getValueAsPrintableString(targetFormat = outputFormat) + val leafGene = (field as PairGene).second.getLeafGene() + val additionalPropertiesVarName = when (leafGene) { + is ObjectGene -> { + val attributeName = leafGene.refType ?: "${dtoName}_ap" + val childDtoCall = + getDtoCall(leafGene, getDtoName(leafGene, attributeName, true), childCounter, true) + result.addAll(childDtoCall.objectCalls) + childDtoCall.varName + } + + is ArrayGene<*> -> { + val attributeName = if (leafGene.template is ObjectGene) { + leafGene.template.refType ?: "${dtoName}_ap" + } else { + getListType("", leafGene.template, true) } - else -> throw IllegalStateException("Additional properties should only be Map related genes") -// else -> { -// val parent = leafGene.parent -// if (leafGene is EnumGene<*> && parent is ChoiceGene<*>) { -// val children = parent.getViewOfChildren() -// val otherChoice = children.find { child -> child != leafGene } -//// result.add(dtoOutput.getSetterStatement(dtoVarName, attributeName, "${leafGene.getValueAsPrintableString(targetFormat = outputFormat)}${getValueSuffix(otherChoice)}")) -// } else { -//// result.add(dtoOutput.getSetterStatement(dtoVarName, attributeName, "${leafGene.getValueAsPrintableString(targetFormat = outputFormat)}${getValueSuffix(leafGene)}")) -// } -// "" -// } + val childDtoCall = getArrayDtoCall( + leafGene, + getDtoName(leafGene, attributeName, true), + childCounter, + attributeName, + true + ) + + result.addAll(childDtoCall.objectCalls) + childDtoCall.varName } - result.add(dtoOutput.getAddElementToAdditionalPropertiesStatement(dtoVarName, key, additionalPropertiesVarName)) - } catch (ex: Exception) { - log.warn( - "A failure has occurred when writing DTOs. \n" - + "Exception: ${ex.localizedMessage} \n" - + "At ${ex.stackTrace.joinToString(separator = " \n -> ")}. " - ) - assert(false) + + else -> throw IllegalStateException("Additional properties should only be Map related genes") } + result.add( + dtoOutput.getAddElementToAdditionalPropertiesStatement( + dtoVarName, + key, + additionalPropertiesVarName + ) + ) + } catch (ex: Exception) { + log.warn( + "A failure has occurred when writing DTOs. \n" + + "Exception: ${ex.localizedMessage} \n" + + "At ${ex.stackTrace.joinToString(separator = " \n -> ")}. " + ) + assert(false) } } } - - return DtoCall(dtoVarName, result) } private fun getArrayDtoCall(gene: ArrayGene<*>, dtoName: String, counters: MutableList, targetAttribute: String?, capitalize: Boolean): DtoCall { diff --git a/core/src/main/kotlin/org/evomaster/core/output/service/HttpWsTestCaseWriter.kt b/core/src/main/kotlin/org/evomaster/core/output/service/HttpWsTestCaseWriter.kt index 9100678e8c..7a8125360a 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/service/HttpWsTestCaseWriter.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/service/HttpWsTestCaseWriter.kt @@ -27,6 +27,7 @@ import org.evomaster.core.search.action.EvaluatedAction import org.evomaster.core.search.gene.Gene import org.evomaster.core.search.gene.ObjectGene import org.evomaster.core.search.gene.collection.ArrayGene +import org.evomaster.core.search.gene.collection.FixedMapGene import org.evomaster.core.search.gene.utils.GeneUtils import org.evomaster.core.search.gene.wrapper.ChoiceGene import org.slf4j.LoggerFactory @@ -138,7 +139,7 @@ abstract class HttpWsTestCaseWriter : ApiTestCaseWriter() { } } else { val leafGene = primaryGene.getLeafGene() - if (leafGene is ObjectGene || leafGene is ArrayGene<*>) { + if (leafGene is ObjectGene || leafGene is ArrayGene<*> || leafGene is FixedMapGene<*,*>) { return generateDtoCall(leafGene, actionName, lines).varName } } From b5bbf2475ff5b3d8bc351c9342e2b795c923eeb7 Mon Sep 17 00:00:00 2001 From: Philip Garrett Date: Thu, 12 Feb 2026 11:12:23 -0300 Subject: [PATCH 5/7] Javadoc and remove comments for additionalProperties --- .../org/evomaster/core/output/dto/DtoClass.kt | 15 +-------------- .../org/evomaster/core/output/dto/DtoOutput.kt | 7 +++++++ .../org/evomaster/core/output/dto/DtoWriter.kt | 2 +- .../evomaster/core/output/dto/JavaDtoOutput.kt | 5 ----- .../evomaster/core/output/dto/KotlinDtoOutput.kt | 2 -- .../org/evomaster/core/search/gene/ObjectGene.kt | 1 - .../evomaster/core/output/dto/DtoWriterTest.kt | 5 ----- 7 files changed, 9 insertions(+), 28 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/DtoClass.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/DtoClass.kt index bfab4ea6dc..f3bcab2403 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/DtoClass.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/DtoClass.kt @@ -2,29 +2,16 @@ package org.evomaster.core.output.dto class DtoClass( val name: String, -// val fields: MutableList = mutableListOf(), val fieldsMap: MutableMap = mutableMapOf(), var hasAdditionalProperties: Boolean = false, -// var additionalProperties: DtoField? = null ) { -// fun addField(field: DtoField) { -// if (field !in fields) fields.add(field) -// } - -// fun hasField(field: DtoField): Boolean { -// return fieldsMap.containsKey(field.name) -// } - lateinit var additionalPropertiesDtoName: String - fun addMapField(fieldName: String, field: DtoField) { + fun addField(fieldName: String, field: DtoField) { if (!fieldsMap.containsKey(fieldName)) { fieldsMap[fieldName] = field } } - - - } diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/DtoOutput.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/DtoOutput.kt index 10bede1aa8..041e9932d5 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/DtoOutput.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/DtoOutput.kt @@ -52,6 +52,13 @@ interface DtoOutput { */ fun getAddElementToListStatement(listVarName: String, value: String): String + /** + * @param additionalPropertiesVarName variable in which the additionalProperties will be added + * @param key for the additional property to be added to the DTO + * @param value variable name representing the additional properties + * + * @return the add additional properties statement + */ fun getAddElementToAdditionalPropertiesStatement(additionalPropertiesVarName: String, key: String, value: String): String } diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt index 03a6a0bf8a..7ab1508f6e 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt @@ -181,7 +181,7 @@ class DtoWriter( try { val wrappedGene = field.getLeafGene() val dtoField = getDtoField(field.name, wrappedGene) - dtoClass.addMapField(field.name, dtoField) + dtoClass.addField(field.name, dtoField) if (wrappedGene is ObjectGene) { calculateDtoFromObject(wrappedGene, dtoField.type) } diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/JavaDtoOutput.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/JavaDtoOutput.kt index 2efeb3a040..b8b3777f47 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/JavaDtoOutput.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/JavaDtoOutput.kt @@ -3,7 +3,6 @@ package org.evomaster.core.output.dto import org.evomaster.core.output.Lines import org.evomaster.core.output.OutputFormat import org.evomaster.core.output.TestSuiteFileName -import org.evomaster.core.utils.StringUtils import java.nio.file.Path class JavaDtoOutput: JvmDtoOutput() { @@ -24,7 +23,6 @@ class JavaDtoOutput: JvmDtoOutput() { } override fun getSetterStatement(dtoVarName: String, attributeName: String, value: String): String { -// return "$dtoVarName.set${StringUtils.capitalization(attributeName)}($value);" return "$dtoVarName.set${capitalizeFirstChar(attributeName)}($value);" } @@ -56,7 +54,6 @@ class JavaDtoOutput: JvmDtoOutput() { } private fun addVariables(lines: Lines, dtoClass: DtoClass) { -// dtoClass.fields.forEach { dtoClass.fieldsMap.forEach { lines.indented { lines.add("@JsonProperty(\"${it.key}\")") @@ -74,11 +71,9 @@ class JavaDtoOutput: JvmDtoOutput() { } private fun addGettersAndSetters(lines: Lines, dtoClass: DtoClass) { -// dtoClass.fields.forEach { dtoClass.fieldsMap.forEach { val varName = it.key val varType = it.value.type -// val capitalizedVarName = StringUtils.capitalization(varName) val capitalizedVarName = capitalizeFirstChar(varName) lines.indented { lines.add("public Optional<${varType}> get${capitalizedVarName}() {") diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/KotlinDtoOutput.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/KotlinDtoOutput.kt index dfe6faf1eb..698a4418ad 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/KotlinDtoOutput.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/KotlinDtoOutput.kt @@ -44,7 +44,6 @@ class KotlinDtoOutput: JvmDtoOutput() { } private fun addVariables(lines: Lines, dtoClass: DtoClass) { -// dtoClass.fields.forEach { dtoClass.fieldsMap.forEach { lines.indented { lines.add("@JsonProperty(\"${it.key}\")") @@ -55,7 +54,6 @@ class KotlinDtoOutput: JvmDtoOutput() { if (dtoClass.hasAdditionalProperties) { lines.indented { lines.add("@JsonIgnore") -// lines.add("val additionalProperties: MutableMap = mutableMapOf()") lines.add("private val additionalProperties: MutableMap = mutableMapOf()") lines.addEmpty() lines.add("@JsonAnyGetter") diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/ObjectGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/ObjectGene.kt index c109f95261..693a826647 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/ObjectGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/ObjectGene.kt @@ -333,7 +333,6 @@ class ObjectGene( if (it.isNotEmpty() && includedFields.isNotEmpty()) buffer.append(", ") }.joinTo(buffer, ", ") { -// val sec = it.second "\"${it.first.value}\":${it.second.getValueAsPrintableString(previousGenes, mode, targetFormat)}" } } diff --git a/core/src/test/kotlin/org/evomaster/core/output/dto/DtoWriterTest.kt b/core/src/test/kotlin/org/evomaster/core/output/dto/DtoWriterTest.kt index e89a6fa25c..790094619f 100644 --- a/core/src/test/kotlin/org/evomaster/core/output/dto/DtoWriterTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/output/dto/DtoWriterTest.kt @@ -108,7 +108,6 @@ class DtoWriterTest { assertEquals(collectedDtos.size, 1) val oneOfDto = collectedDtos[collectedDtos.keys.first()] assertNotNull(oneOfDto) -// val dtoFields = oneOfDto?.fields?:emptyList() val dtoFields = oneOfDto?.fieldsMap?:emptyMap() assertEquals(dtoFields.size, 2) assertDtoFieldIn(dtoFields, "dog", STRING) @@ -127,7 +126,6 @@ class DtoWriterTest { assertEquals(collectedDtos.size, 1) val anyOfDto = collectedDtos[collectedDtos.keys.first()] assertNotNull(anyOfDto) -// val dtoFields = anyOfDto?.fields?:emptyList() val dtoFields = anyOfDto?.fieldsMap?:emptyMap() assertEquals(dtoFields.size, 2) assertDtoFieldIn(dtoFields, "email", STRING) @@ -145,7 +143,4 @@ class DtoWriterTest { assertThat(dtoFields[targetName], `is`(DtoField(targetName, targetType))) } - private fun assertDtoFieldIn(dtoFields: List, targetName: String, targetType: String) { - assertThat(dtoFields, hasItem(DtoField(targetName, targetType))) - } } From 46d19a7edaedda5ae143945546915d44b3cf8ffe Mon Sep 17 00:00:00 2001 From: Philip Garrett Date: Sun, 15 Feb 2026 10:01:54 -0300 Subject: [PATCH 6/7] Changes requested --- .../org/evomaster/core/output/dto/DtoClass.kt | 9 +- .../evomaster/core/output/dto/DtoWriter.kt | 44 ++++----- .../evomaster/core/output/dto/GeneToDto.kt | 99 ++++++++++--------- .../core/output/dto/JavaDtoOutput.kt | 12 ++- .../evomaster/core/output/dto/JvmDtoOutput.kt | 9 -- .../core/output/dto/KotlinDtoOutput.kt | 11 ++- .../org/evomaster/core/utils/StringUtils.kt | 24 +++++ 7 files changed, 123 insertions(+), 85 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/DtoClass.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/DtoClass.kt index f3bcab2403..abb4ce12f9 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/DtoClass.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/DtoClass.kt @@ -3,10 +3,11 @@ package org.evomaster.core.output.dto class DtoClass( val name: String, val fieldsMap: MutableMap = mutableMapOf(), - var hasAdditionalProperties: Boolean = false, +// var hasAdditionalProperties: Boolean = false, + var additionalPropertiesDtoName: String? = null ) { - lateinit var additionalPropertiesDtoName: String +// private lateinit var additionalPropertiesDtoName: String fun addField(fieldName: String, field: DtoField) { if (!fieldsMap.containsKey(fieldName)) { @@ -14,4 +15,8 @@ class DtoClass( } } + fun hasAdditionalProperties(): Boolean { + return !additionalPropertiesDtoName.isNullOrEmpty() + } + } diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt index 76b9138983..64d229d788 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt @@ -194,29 +194,29 @@ class DtoWriter( } private fun addAdditionalProperties(dtoClass: DtoClass, additionalProperties: List>) { - if (additionalProperties.isNotEmpty()) { - dtoClass.hasAdditionalProperties = true - additionalProperties.forEach { field -> - try { - val wrappedGene = (field as PairGene).second.getLeafGene() - if (wrappedGene is ObjectGene) { - val additionalPropertiesDtoName = wrappedGene.refType?:"${dtoClass.name}_ap" - dtoClass.additionalPropertiesDtoName = additionalPropertiesDtoName - calculateDtoFromObject(wrappedGene, additionalPropertiesDtoName) - } - if (wrappedGene is ArrayGene<*> && wrappedGene.template is ObjectGene) { - val additionalPropertiesDtoName = wrappedGene.template.refType?:"${dtoClass.name}_ap" - dtoClass.additionalPropertiesDtoName = additionalPropertiesDtoName - calculateDtoFromObject(wrappedGene.template, additionalPropertiesDtoName) - } - } catch (ex: Exception) { - log.warn( - "A failure has occurred when collecting DTO additional properties. \n" - + "Exception: ${ex.localizedMessage} \n" - + "At ${ex.stackTrace.joinToString(separator = " \n -> ")}. " - ) - assert(false) + if (additionalProperties.isEmpty()) { + return + } + additionalProperties.forEach { field -> + try { + val wrappedGene = (field as PairGene).second.getLeafGene() + if (wrappedGene is ObjectGene) { + val additionalPropertiesDtoName = wrappedGene.refType?:"${dtoClass.name}_ap" + dtoClass.additionalPropertiesDtoName = additionalPropertiesDtoName + calculateDtoFromObject(wrappedGene, additionalPropertiesDtoName) } + if (wrappedGene is ArrayGene<*> && wrappedGene.template is ObjectGene) { + val additionalPropertiesDtoName = wrappedGene.template.refType?:"${dtoClass.name}_ap" + dtoClass.additionalPropertiesDtoName = additionalPropertiesDtoName + calculateDtoFromObject(wrappedGene.template, additionalPropertiesDtoName) + } + } catch (ex: Exception) { + log.warn( + "A failure has occurred when collecting DTO additional properties. \n" + + "Exception: ${ex.localizedMessage} \n" + + "At ${ex.stackTrace.joinToString(separator = " \n -> ")}. " + ) + assert(false) } } } diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt index 5cffdf2fc5..c4f81f38f6 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt @@ -149,60 +149,61 @@ class GeneToDto( } private fun setAdditionalProperties(dtoName: String, dtoVarName: String, result: MutableList, counters: MutableList, additionalProperties: List>) { - if (additionalProperties.isNotEmpty()) { - var additionalPropertiesCounter = 1 - additionalProperties.forEach { field -> - try { - val childCounter = mutableListOf() - childCounter.addAll(counters) - childCounter.add(additionalPropertiesCounter++) - val key = (field as PairGene).first.getLeafGene() - .getValueAsPrintableString(targetFormat = outputFormat) - val leafGene = (field as PairGene).second.getLeafGene() - val additionalPropertiesVarName = when (leafGene) { - is ObjectGene -> { - val attributeName = leafGene.refType ?: "${dtoName}_ap" - val childDtoCall = - getDtoCall(leafGene, getDtoName(leafGene, attributeName, true), childCounter, true) - result.addAll(childDtoCall.objectCalls) - childDtoCall.varName - } - - is ArrayGene<*> -> { - val attributeName = if (leafGene.template is ObjectGene) { - leafGene.template.refType ?: "${dtoName}_ap" - } else { - getListType("", leafGene.template, true) - } - val childDtoCall = getArrayDtoCall( - leafGene, - getDtoName(leafGene, attributeName, true), - childCounter, - attributeName, - true - ) + if (additionalProperties.isEmpty()) { + return + } + var additionalPropertiesCounter = 1 + additionalProperties.forEach { field -> + try { + val childCounter = mutableListOf() + childCounter.addAll(counters) + childCounter.add(additionalPropertiesCounter++) + val key = (field as PairGene).first.getLeafGene() + .getValueAsPrintableString(targetFormat = outputFormat) + val leafGene = (field as PairGene).second.getLeafGene() + val additionalPropertiesVarName = when (leafGene) { + is ObjectGene -> { + val attributeName = leafGene.refType ?: "${dtoName}_ap" + val childDtoCall = + getDtoCall(leafGene, getDtoName(leafGene, attributeName, true), childCounter, true) + result.addAll(childDtoCall.objectCalls) + childDtoCall.varName + } - result.addAll(childDtoCall.objectCalls) - childDtoCall.varName + is ArrayGene<*> -> { + val attributeName = if (leafGene.template is ObjectGene) { + leafGene.template.refType ?: "${dtoName}_ap" + } else { + getListType("", leafGene.template, true) } + val childDtoCall = getArrayDtoCall( + leafGene, + getDtoName(leafGene, attributeName, true), + childCounter, + attributeName, + true + ) - else -> throw IllegalStateException("Additional properties should only be Map related genes") + result.addAll(childDtoCall.objectCalls) + childDtoCall.varName } - result.add( - dtoOutput.getAddElementToAdditionalPropertiesStatement( - dtoVarName, - key, - additionalPropertiesVarName - ) - ) - } catch (ex: Exception) { - log.warn( - "A failure has occurred when writing DTOs. \n" - + "Exception: ${ex.localizedMessage} \n" - + "At ${ex.stackTrace.joinToString(separator = " \n -> ")}. " - ) - assert(false) + + else -> throw IllegalStateException("Additional properties should only be Map related genes") } + result.add( + dtoOutput.getAddElementToAdditionalPropertiesStatement( + dtoVarName, + key, + additionalPropertiesVarName + ) + ) + } catch (ex: Exception) { + log.warn( + "A failure has occurred when writing DTOs. \n" + + "Exception: ${ex.localizedMessage} \n" + + "At ${ex.stackTrace.joinToString(separator = " \n -> ")}. " + ) + assert(false) } } } diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/JavaDtoOutput.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/JavaDtoOutput.kt index fc14c62995..f5ad648d3d 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/JavaDtoOutput.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/JavaDtoOutput.kt @@ -3,6 +3,7 @@ package org.evomaster.core.output.dto import org.evomaster.core.output.Lines import org.evomaster.core.output.OutputFormat import org.evomaster.core.output.TestSuiteFileName +import org.evomaster.core.utils.StringUtils.capitalizeFirstChar import java.nio.file.Path class JavaDtoOutput: JvmDtoOutput() { @@ -61,8 +62,13 @@ class JavaDtoOutput: JvmDtoOutput() { } lines.addEmpty() } - if (dtoClass.hasAdditionalProperties) { + if (dtoClass.hasAdditionalProperties()) { lines.indented { + /* + * We ignore additionalProperties map since otherwise Jackson will attempt to serialize it as + * { ..., "additionalProperties": { ... } } + * Where actually what we need is the inner object with the different key-values. + */ lines.add("@JsonIgnore") lines.add("private Map additionalProperties = new HashMap<>();") } @@ -90,8 +96,9 @@ class JavaDtoOutput: JvmDtoOutput() { } lines.addEmpty() } - if (dtoClass.hasAdditionalProperties) { + if (dtoClass.hasAdditionalProperties()) { lines.indented { + // Ensures that entries stored in additionalProperties are flattened into the JSON object serialization. lines.add("@JsonAnyGetter") lines.add("public Map getAdditionalProperties() {") lines.indented { @@ -99,6 +106,7 @@ class JavaDtoOutput: JvmDtoOutput() { } lines.add("}") lines.addEmpty() + // Allows the DTO to accept JSON properties that are not declared as explicit fields. lines.add("@JsonAnySetter") lines.add("public void addAdditionalProperty(String name, ${dtoClass.additionalPropertiesDtoName} value) {") lines.indented { diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/JvmDtoOutput.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/JvmDtoOutput.kt index ec7aa94268..eaa843aaae 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/JvmDtoOutput.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/JvmDtoOutput.kt @@ -44,13 +44,4 @@ abstract class JvmDtoOutput: DtoOutput { path.toFile().appendText(testFileContent) } - fun capitalizeFirstChar(word: String) : String { - if(word.isEmpty()){ - return word - } - - return word.substring(0, 1).uppercase() + - word.substring(1) - } - } diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/KotlinDtoOutput.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/KotlinDtoOutput.kt index 1a032c9dba..33adf142aa 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/KotlinDtoOutput.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/KotlinDtoOutput.kt @@ -51,11 +51,18 @@ class KotlinDtoOutput: JvmDtoOutput() { } lines.addEmpty() } - if (dtoClass.hasAdditionalProperties) { + if (dtoClass.hasAdditionalProperties()) { lines.indented { + /* + * We ignore additionalProperties map since otherwise Jackson will attempt to serialize it as + * { ..., "additionalProperties": { ... } } + * Where actually what we need is the inner object with the different key-values. + */ lines.add("@JsonIgnore") lines.add("private val additionalProperties: MutableMap = mutableMapOf()") lines.addEmpty() + + // Ensures that entries stored in additionalProperties are flattened into the JSON object serialization. lines.add("@JsonAnyGetter") lines.add("fun getAdditionalProperties(): MutableMap {") lines.indented { @@ -63,6 +70,8 @@ class KotlinDtoOutput: JvmDtoOutput() { } lines.add("}") lines.addEmpty() + + // Allows the DTO to accept JSON properties that are not declared as explicit fields. lines.add("@JsonAnySetter") lines.add("fun addAdditionalProperty(name: String, value: ${dtoClass.additionalPropertiesDtoName}) {") lines.indented { diff --git a/core/src/main/kotlin/org/evomaster/core/utils/StringUtils.kt b/core/src/main/kotlin/org/evomaster/core/utils/StringUtils.kt index 8089c91ee1..426d1a8737 100644 --- a/core/src/main/kotlin/org/evomaster/core/utils/StringUtils.kt +++ b/core/src/main/kotlin/org/evomaster/core/utils/StringUtils.kt @@ -4,6 +4,13 @@ import java.util.* object StringUtils { + /** + * Capitalizes a word, lowercasing the rest of the word. For example, stringProperty would be modified into + * Stringproperty. + * + * @param word to be capitalized + * @return the capitalized word + */ fun capitalization(word: String) : String{ if(word.isEmpty()){ return word @@ -13,6 +20,23 @@ object StringUtils { word.substring(1).lowercase() } + /** + * Capitalizes the first char of a word, without modifying the rest. For example, stringProperty would be + * modified into StringProperty. This is useful for writing setter methods in output, where the name matches + * the property being serialized. + * + * @param word to be capitalized + * @return the capitalized word + */ + fun capitalizeFirstChar(word: String) : String { + if(word.isEmpty()){ + return word + } + + return word.substring(0, 1).uppercase() + + word.substring(1) + } + /** * A function for extracting the Simple Class Name from a class string. Given an inner class in the * OuterClass$InnerClass format, it will return the InnerClass value From 2e2d74b8fd4c34735f566bd057b658266703039f Mon Sep 17 00:00:00 2001 From: Philip Garrett Date: Sun, 15 Feb 2026 10:03:35 -0300 Subject: [PATCH 7/7] Remove commented code --- core/src/main/kotlin/org/evomaster/core/output/dto/DtoClass.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/DtoClass.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/DtoClass.kt index abb4ce12f9..09c68c38be 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/DtoClass.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/DtoClass.kt @@ -3,12 +3,9 @@ package org.evomaster.core.output.dto class DtoClass( val name: String, val fieldsMap: MutableMap = mutableMapOf(), -// var hasAdditionalProperties: Boolean = false, var additionalPropertiesDtoName: String? = null ) { -// private lateinit var additionalPropertiesDtoName: String - fun addField(fieldName: String, field: DtoField) { if (!fieldsMap.containsKey(fieldName)) { fieldsMap[fieldName] = field