Skip to content

Json patch, initial pr, structures only#1525

Open
suarezrominajulieta wants to merge 25 commits into
masterfrom
feature/jsonPatch
Open

Json patch, initial pr, structures only#1525
suarezrominajulieta wants to merge 25 commits into
masterfrom
feature/jsonPatch

Conversation

@suarezrominajulieta
Copy link
Copy Markdown
Collaborator

@suarezrominajulieta suarezrominajulieta commented Apr 26, 2026

Summary

Introduces the foundational gene structure and builder needed to model JSON Patch

New files

Gene model (core/src/main/kotlin/.../search/gene/patch/)

  • JsonPatchOperationGene — enum-backed gene representing a single RFC 6902 op (add, remove, replace, move, copy, test)
  • JsonPatchPathOnlyGene — composite gene for ops that only require a path field (remove)
  • JsonPatchPathValueGene — composite gene for ops that require path + value (add, replace, test)
  • JsonPatchFromPathGene — composite gene for ops that require path + from (move, copy)
  • JsonPatchDocumentGene — top-level gene representing a full JSON Patch document (array of operation objects); handles serialization to JSON and mutation logic

Builder (core/src/main/kotlin/.../problem/rest/builder/)

  • JsonPatchGeneBuilder — constructs a JsonPatchDocumentGene from an OpenAPI schema, mapping PATCH request body fields to the appropriate sub-gene variant

Tests (core/src/test/kotlin/...)

  • Unit tests for each gene class and the builder
  • GeneSamplerForTests and GeneNumberOfGenesTest updated to include new gene types

What's NOT included yet

  • Integration with RestActionBuilderV3 (wiring the builder into the main action pipeline)
  • E2E test against a real PATCH endpoint

Notes

This is a structural PR — all new classes compile and their unit tests pass, but the genes are not yet invoked during actual fuzzing runs.

@suarezrominajulieta suarezrominajulieta marked this pull request as ready for review April 27, 2026 11:33
@jgaleotti
Copy link
Copy Markdown
Collaborator

Hi @suarezrominajulieta, first ask for my review and then, when I aprove the PR, I will request @arcuri82 's review.

@suarezrominajulieta suarezrominajulieta removed the request for review from arcuri82 April 27, 2026 16:45
@suarezrominajulieta
Copy link
Copy Markdown
Collaborator Author

suarezrominajulieta commented Apr 27, 2026

Hi @jgaleotti ! Okey, done :)

* Provides heuristics that extract valid JSON Pointer paths from a resource schema
* and group them by field type to construct type-safe path-value pairs.
*/
object JsonPatchGeneBuilder {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why it is saying that it provides "heuristics"? This class deterministically creates an JsonPathGene from a given schema

resourceSchema: Gene?
): ArrayGene<ChoiceGene<JsonPatchOperationGene>> {

val allPaths = JsonPatchGeneBuilder.extractAllPaths(resourceSchema)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happens if resourceSchema is null?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can/should we let it do nothing for this pr?

val choices = mutableListOf<JsonPatchOperationGene>()

// Operations that only need a path
choices.add(JsonPatchPathOnlyGene("remove", pathEnum.copy() as EnumGene<String>))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replace "remove" and other strings with constants


// Operations that need path + value; pairs are grouped by schema field type
val pathValueEntries = JsonPatchGeneBuilder.buildPathValueEntries(allPaths)
val effectiveEntries: List<PairGene<EnumGene<String>, Gene>> =
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does effectiveEntries mean? Shouldn't this list be called pathValuePairGenes?

val fromGene: EnumGene<String>,
val pathGene: EnumGene<String>,
geneName: String = "${operationName}Op"
) : JsonPatchOperationGene(geneName, operationName, listOf(fromGene, pathGene)) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check that the operation name is move or copy. Why does it add the "Op" ?

* Provides heuristics that extract valid JSON Pointer paths from a resource schema
* and group them by field type to construct type-safe path-value pairs.
*/
object JsonPatchGeneBuilder {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not include the schema extraction heuristics in this PR

class JsonPatchPathOnlyGene(
operationName: String,
val pathGene: EnumGene<String>,
geneName: String = "${operationName}Op"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we have operationName and geneName?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what I should implement. Could you explain again what we were expecting here?

I created the operationName property, which inherits from the abstract class, to verify and distinguish between a pathfrom that is a move and a pathfrom that is a copy, and to ensure that each is printed correctly.

And then there's the geneName or name, which should be the name of the gene, which can be anything, like "Gene1".

I'm wondering if the child classes should only have operationName and not name, or if we should keep both, but make them exactly the same as those defined by the parent class in the inheritance.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understood why we have both. Please keep geneName (name) and operationName.

targetFormat: OutputFormat?,
extraCheck: Boolean
): String {
val activePair = pathValueChoice.activeGene()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as previous comments

@suarezrominajulieta
Copy link
Copy Markdown
Collaborator Author

Hi @jgaleotti ! i added the following as previously talked:

  1. the resourceSchema logic when given, or random if not and it's tests (and value entries with random string or integer genes also).
  2. and the whole json document tests for json and xml printing.

I'd appreciate it if you could review it now, thanks!

* a ChoiceGene that selects among the six standard operations:
* remove, move, copy, add, replace, test.
*
* [resourceSchema] is stored for future schema-aware path extraction but is not yet analysed.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update this javadoc

// --- XML serialization ---

@Test
fun testRemoveOperationXmlFormat() {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a test case checking JSON Format for this

}

@Test
fun testMoveOperationXmlFormat() {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a test case checking JSON Format

extraCheck: Boolean
): String {
val inner = operationsArray.getValueAsPrintableString(previousGenes, mode, targetFormat, extraCheck)
return if (mode == GeneUtils.EscapeMode.XML) "<patch>$inner</patch>" else inner
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add comment regarding lack of XML convention for JSON Patch documents written in XML

*/
class JsonPatchDocumentGene private constructor(
name: String,
val resourceSchema: Gene?,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems to me that the resourceSchema does not need to be stored

extraCheck: Boolean
): String = formatOperation(
mode,
OpField("from", fromGene.getValueAsRawString()),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't this be fromGene.getValueAsPrintableString()

}
}

override fun getValueAsPrintableString(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getValueAsPrintableString should be defined in the superclass JsonPatchOperationGene

/** A single field in a JSON Patch operation, with its serialized value.
* [quoted] controls whether the value is wrapped in quotes in JSON output
* (true for string fields like path/from; false for typed fields like value). */
protected data class OpField(val name: String, val value: String, val quoted: Boolean = true)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this class is not actually needed

@suarezrominajulieta
Copy link
Copy Markdown
Collaborator Author

Hi, @jgaleotti i made the changes we last talked about:

  1. Add a comment stating that we are not implementing a convention, because there isn't one, by using patch as the root's nametag.
  2. We can remove the resource schema: receive it, build the template with it, and discard it.
  3. The parent operation should only have getValueAsPrintableString, and the children should not:

I had to keep getValueAsRawString with EnumGene because getValueAsPrintableString in enums, prints with "", i.e: "/example", and it would print an xml like this "/x" instead of /x.

ty!

*/
fun buildOperationsArray(
resourceSchema: Gene? = null,
randomness: Randomness? = null
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't remember discussing the creation of random paths when no resourceSchema is provided. I am not very confortable having a "nondetermintisc" gene creation.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, but what should we do in case there's no schema provided, how do we build the enum genes for the paths?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we make it non nulleable, and throw an exception if not provided?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's try having a schema by default as you used to have (the one with /a, /b, etc).
What do you think?

// private helpers
// ---------------------------------------------------------------------------

private fun generateRandomPaths(randomness: Randomness): List<String> =
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure about this renadom path creation. I think this builder should be completely deterministic.

private fun buildFromPaths(
paths: List<String>
): ArrayGene<ChoiceGene<JsonPatchOperationGene>> {
val pathEnum = EnumGene<String>("path", paths)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use constants instead of strings such as "path", "entry_string", etc.

targetFormat: OutputFormat?,
extraCheck: Boolean
): String {
// path/from fields use getValueAsRawString() instead of getValueAsPrintableString() because
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am still not sure about using getValueAsRawString(). As an example, what happens if I have an ObjectGene with an EnumGene?


private fun children(randomness: Randomness = rand): List<JsonPatchOperationGene> =
JsonPatchDocumentGeneBuilder.buildOperationsArray(randomness = randomness)
.template.getViewOfChildren().map { it as JsonPatchOperationGene }
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not like builder using randomness.

val paths = (removeOp.pathGene as EnumGene<*>).values.map { it.toString() }
assertTrue(paths.isNotEmpty() && paths.all { it.startsWith("/") })
}
} No newline at end of file
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add test cases for the scenario where there are multiple paths with the same value type

// wrong quotes inside XML tags (<path>"/x"</path>). getValueAsRawString() returns the
// bare string (e.g. /x), letting us control quoting per format ourselves.
val fields = when (this) {
is JsonPatchPathOnlyGene -> {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is possible to have this implementation without referring explicitly to each subclass using the ``children'' fields

// EnumGene<String>.getValueAsPrintableString() always wraps the value in quotes regardless of
// mode or targetFormat. Using it would produce double-quoting in JSON ("path":""/x"") and
// wrong quotes inside XML tags (<path>"/x"</path>). getValueAsRawString() returns the
// bare string (e.g. /x), letting us control quoting per format ourselves.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is strange since I am not seeing that this is actually happening when having enumgenes in an objectgene

@Test
fun testObjectGeneWithEnumGene() {
    val name = "com.example.MyDto"
    val schema = """
        "$name": {
            "type": "object",
            "properties": {
                "path": {
                    "type": "string",
                    "enum": ["/some", "/some/path", "/some/other/path"]
                }
            },
            "required": ["path"]
        }
    """.trimIndent()

    val gene = RestActionBuilderV3.createGeneForDTO(name, schema, name, RestActionBuilderV3.Options())

    assertTrue(gene is ObjectGene)
    val objectGene = gene as ObjectGene
    val field = objectGene.fields.find { it.name == "path" }
    assertNotNull(field)
    assertTrue(ParamUtil.getValueGene(field!!) is EnumGene<*>)
    val enumGene = ParamUtil.getValueGene(field) as EnumGene<String>
    assertEquals(3, enumGene.values.size)
    assertTrue(enumGene.values.containsAll(listOf("/some", "/some/path", "/some/other/path")))

    val json = objectGene.getValueAsPrintableString(listOf(), GeneUtils.EscapeMode.JSON, OutputFormat.JAVA_JUNIT_5)
    assertTrue(json == """{"path":"/some"}""" || json == """{"path":"/some/path"}""" || json == """{"path":"/some/other/path"}""")
}

Copy link
Copy Markdown
Collaborator Author

@suarezrominajulieta suarezrominajulieta May 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you are rigth, this prints okey for json, adding the "" to the string printed (not double-quoting, that's wrong in my comment/possible implementation) => assertTrue(enumGene.values.containsAll(listOf("/some", "/some/path", "/some/other/path"))) ok.

but for xml, enumGene.getValueAsPrintableString prints the same as json, with the simple quoting, resulting in, for ex.: <operation><op>remove</op><path>"/some/path"</path></operation>, which is not xml correct.

val xmlPrintable = enumGene.getValueAsPrintableString(mode = GeneUtils.EscapeMode.XML)
assertEquals("\"/age\"", xmlPrintable)

that's why i used raw, so for json i add the simple quoting and in xml, they don't appear. but maybe we should fix the mode printing in enumgene?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants