Skip to content

AsyncAPI 3.x: build genes from message payloads - #1710

Merged
arcuri82 merged 2 commits into
masterfrom
feature/asyncapi-payload-genes
Sep 9, 2026
Merged

AsyncAPI 3.x: build genes from message payloads#1710
arcuri82 merged 2 commits into
masterfrom
feature/asyncapi-payload-genes

Conversation

@LautaroPetaccio

@LautaroPetaccio LautaroPetaccio commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Seventh in the AsyncAPI stack, on top of #5. The first piece that turns the parsed document into something the search can hold.

The seam

The parser deliberately stops at the schema: it leaves every $ref inside a payload alone, and guarantees that whatever those references reach is present in componentSchemas. This trades on that guarantee — it hands the whole schema map to RestActionBuilderV3.createGeneForDTO, which wraps it in a synthetic OpenAPI document and lets the existing machinery resolve the references and build the genes.

Reusing that rather than writing a second JSON-Schema-to-gene converter means AsyncAPI payloads get what REST already has. I checked rather than assumed, by characterising the seam against the shapes AsyncAPI actually uses before writing anything:

shape result
object with required required fields plain, others OptionalGene
$ref to a component schema resolved and inlined
minimum / maximum keptIntegerGene [min=3, max=1000]
oneOf ChoiceGene over both branches
self-referencing schema terminates via CycleObjectGene
nullable: true NullableGene
type: [object, "null"] object read correctly, but nullability dropped
const lost — becomes a free string

The bounds one matters most: NCS's error reply is only reachable because n is declared minimum: 3, so the boundary has to survive into the gene for the search to find it. It does.

const

const is invisible to the gene builder, with or without an explicit type beside it. That is not an exotic corner: const is how a document says which message a payload is (request: {const: list_legs} — voiceblender does this across 109 operations, and five documents in the wider corpus use it). Left alone, the discriminator is a random string and the service never recognises the message.

It is rewritten to the single-valued enum that means the same thing, with the type inferred from the literal when the schema states none. A boolean const is the exception and stays free, since there is no enum handling for booleans — asserted in a test rather than left to be discovered.

Why invalidData is off for payloads

That flag adds a bogus member to every enum on purpose, to probe how a service handles a value it never declared. In a message payload it backfires: a const arrives here as a one-value enum, and a message carrying an unrecognised discriminator is silently dropped. Half the messages sent would go nowhere — wasting executions, and firing the no-reply fault target for a fault of our own making.

Nothing is lost for reaching error paths, since the bounds that drive them are preserved.

One change to shared REST code

Finding the above exposed an inconsistency, fixed here. The bogus member added to a string enum is guarded by options.invalidData:

if (options.invalidData) { add("EVOMASTER") }

but the 42, 42L and 42.0 added to integer, long and number enums were not — they were added even with invalid data switched off. They are now guarded the same way. Since allowInvalidData defaults to true, default behaviour is unchanged; this only takes effect for a caller that explicitly asks for valid data, which is what this PR does.

Happy to split that into its own PR if you would rather.

Testing

13 new tests in AsyncApiGeneBuilderTest, including one that builds a gene for every message of a real document and one that asserts building genes does not rewrite the parsed model underneath. 344 in total across the AsyncAPI package and the REST suites that exercise the shared builder — RestActionBuilderV3Test (155), HttpSemanticsOracleTest (55), SchemaUtilsTest, RestSchemaOraclesTest, BlackBoxUtilsTest, JsonPatchSchemaResolverTest.

@LautaroPetaccio
LautaroPetaccio force-pushed the feature/asyncapi-payload-genes branch from 3a366e5 to d1b60f1 Compare August 25, 2026 16:11
@jgaleotti
jgaleotti force-pushed the feature/asyncapi-payload-genes branch from d1b60f1 to 82d7aec Compare September 4, 2026 17:27
@LautaroPetaccio
LautaroPetaccio force-pushed the feature/asyncapi-payload-genes branch from 82d7aec to 350ee26 Compare September 4, 2026 18:44
@LautaroPetaccio
LautaroPetaccio force-pushed the feature/asyncapi-payload-genes branch from 350ee26 to ebfa23e Compare September 4, 2026 19:18
@LautaroPetaccio
LautaroPetaccio force-pushed the feature/asyncapi-payload-genes branch from ebfa23e to e59001b Compare September 4, 2026 19:27
@LautaroPetaccio
LautaroPetaccio force-pushed the feature/asyncapi-payload-genes branch from e59001b to 926e065 Compare September 4, 2026 19:44
@LautaroPetaccio
LautaroPetaccio force-pushed the feature/asyncapi-payload-genes branch from 926e065 to 7dec46a Compare September 4, 2026 20:08
@LautaroPetaccio
LautaroPetaccio force-pushed the feature/asyncapi-payload-genes branch from 7dec46a to 1c6a142 Compare September 4, 2026 20:13
@LautaroPetaccio
LautaroPetaccio force-pushed the feature/asyncapi-payload-genes branch from 1c6a142 to c35949e Compare September 4, 2026 20:22
@LautaroPetaccio
LautaroPetaccio force-pushed the feature/asyncapi-payload-genes branch from c35949e to e7e5688 Compare September 4, 2026 20:32
@LautaroPetaccio
LautaroPetaccio force-pushed the feature/asyncapi-payload-genes branch from e7e5688 to 115ea84 Compare September 4, 2026 21:24
@jgaleotti
jgaleotti requested a review from arcuri82 September 7, 2026 15:03
@arcuri82
arcuri82 force-pushed the feature/asyncapi-payload-genes branch from 115ea84 to b98badc Compare September 8, 2026 11:35
@arcuri82
arcuri82 force-pushed the feature/asyncapi-payload-genes branch from b98badc to 15c91e9 Compare September 8, 2026 11:41
@arcuri82
arcuri82 force-pushed the feature/asyncapi-payload-genes branch from 15c91e9 to c2c3192 Compare September 8, 2026 11:42
Base automatically changed from feature/asyncapi-real-documents to master September 8, 2026 11:45
@arcuri82
arcuri82 force-pushed the feature/asyncapi-payload-genes branch from c2c3192 to f7ffd53 Compare September 8, 2026 11:45
* The keywords whose value maps arbitrary names to schemas. Their keys come from the
* document, so a field a service happens to call "const" or "default" must still be walked.
*/
private val SCHEMA_MAPS = setOf("properties", "patternProperties", "definitions", "\$defs")

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.

fields should not be at bottom of file

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.

Done!

First use of the asyncapi-parser module inside EvoMaster, and the first piece
of AsyncAPI support that is EvoMaster-specific rather than a parser: turning
a message's JSON Schema into genes.

The parser stops at the schema: it leaves every $ref inside a payload alone
and guarantees that whatever those references reach is in componentSchemas.
This is what trades on that guarantee, handing the whole schema map to
RestActionBuilderV3.createGeneForDTO, which wraps it in a synthetic OpenAPI
document and lets the existing machinery resolve the references and build
the genes.

Reusing that rather than writing a second JSON-Schema-to-gene converter means
AsyncAPI payloads get what REST already has, and the tests check the parts
that matter: numeric bounds (a field declared minimum 3 keeps it, which is
what makes NCS's error reply reachable at all), oneOf as a choice, references
resolved through the map, required versus optional fields, and a schema that
refers to itself terminating rather than recursing.

Headers are built by the same code but separately from the payload, since
they travel separately on the wire.

The one thing needing care is `const`, which is invisible to the gene builder
with or without an explicit type beside it. That matters because `const` is
how a document says which message a payload is (request: {const: list_legs});
left alone, the discriminator would be random and the service would not
recognise the message.

It is rewritten into whatever pins the field in a form the builder does read,
and which form that is depends on the literal:

 - text becomes a single-valued enum. The builder adds a bogus member to a
   string enum when asked for invalid data, so payloads are built with that
   off: an unrecognised discriminator is silently dropped, so half the
   messages sent would go nowhere, wasting executions and firing the no-reply
   fault target for a fault of our own making. Nothing is lost for reaching
   error paths, since the bounds that drive them are preserved.
 - numbers become equal bounds instead. The bogus member added to a numeric
   enum is not conditional on anything, so an enum of one value would always
   come back as two.
 - booleans are left alone. There is no enum handling for them and no bounds
   to set, and a two-valued field is guessed half the time anyway. Asserted
   in a test rather than left to be discovered.

No shared REST code is touched.

The four documents these tests read are copied into core's own test
resources rather than reached for in the parser module: a module's test
resources are not on another module's test classpath, and publishing a
test-jar for evomaster-core is noted in the root pom as causing more trouble
than it is worth.
Review feedback: DATA_KEYWORDS and SCHEMA_MAPS were declared after every
function, beside the one that uses them. docs/for_developers.md puts fields
first, then constructors, then methods, and INLINE_PREFIX already followed
that; the two sets now sit with it.
@LautaroPetaccio
LautaroPetaccio force-pushed the feature/asyncapi-payload-genes branch from 02092a1 to f0077ca Compare September 8, 2026 17:19
@arcuri82
arcuri82 merged commit 081b0c4 into master Sep 9, 2026
31 checks passed
@arcuri82
arcuri82 deleted the feature/asyncapi-payload-genes branch September 9, 2026 10:52
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.

3 participants