Skip to content

feat: synthesise response bodies from OpenAPI schemas - #132

Open
MaxMichel2 wants to merge 2 commits into
fix/openapi-ref-resolutionfrom
feat/openapi-schema-synthesis
Open

MaxMichel2 wants to merge 2 commits into
fix/openapi-ref-resolutionfrom
feat/openapi-schema-synthesis

Conversation

@MaxMichel2

Copy link
Copy Markdown
Collaborator

Summary

Stacked on #131 (PR chain: #123 → #124 → #127 → #128 → #129 → #126 → #131 → this) — merge in order.

The OpenAPI parser had no schema support at all — a status code with a declared content.<mediaType>.schema but no examples showed zero mockable variants. This adds response-body synthesis for exactly that case, deliberately narrow (not full JSON Schema conformance): primitives, enum, object, array, allOf (merged), and oneOf (first variant) — see docs/modules/networkmock-core.md's new "Schema-based response synthesis" section for the exact per-shape rules.

What changed

  • SchemaObject/DiscriminatorObject (OpenApiDocument.kt): follow the existing $ref-capable-object style (HeaderObject/ParameterObject). ComponentsObject gains a schemas map; MediaTypeObject gains a schema field.
  • SchemaSynthesizer (new file, openapi/SchemaSynthesizer.kt, internal object): synthesize(schema, resolveSchema) recursively resolves $refs via a caller-supplied callback (reusing OpenApiParser's own ref-resolution machinery from fix: resolve dollar-ref chains and disambiguate component sections #131 — a new ParseContext.resolveSchema resolves against components.schemas) and produces one plausible JsonElement per schema shape.
  • ResponseContent (new sealed interface, OpenApiParser.kt): generalizes ResolvedResponse's file-path-only content into FromFile/Synthesized, so a synthesized body flows through the same response index as a file-backed one, cached alongside it (no regeneration per discovery pass).
  • resolveResponseIndex: per-media-type example resolution extracted into a new resolveMediaTypeResponses helper (keeps the function under detekt's LongMethod threshold) — for each media type, declared examples are resolved first; only if none are declared and a schema is present does synthesis kick in. A status code with any declared examples for a media type never falls back to synthesis for that media type, even if it also declares a schema.
  • MockResponse.isSynthesized: Boolean (default false, additive): threaded through MockConfigRepository.loadResponseFromPath. The operation picker page shows a small "Generated" badge (MockItem.kt) on a synthesized response's row, styled after EndpointCard's method badge (Box + RoundedCornerShape(4.dp) + labelSmall).

Scope decisions (per the handoff)

  • nullable is read but ignored — a real value is always synthesized, never JSON null; this library mocks responses, it doesn't exercise null-handling.
  • oneOf + discriminator: synthesizes the first declared variant regardless of whether a discriminator is present. There's no concrete request/response data at spec-parse time to disambiguate against, so full discriminator-based variant selection isn't attempted — noted here rather than built speculatively.
  • allOf conflicts: two members declaring the same property with different schemas throws a clear error rather than silently picking one.
  • Composition (allOf/oneOf) didn't turn out messier than expected against the test fixtures tried, so this shipped as scoped rather than being cut down to primitives/object/array only.

Public API

api.txt regenerated for devview-networkmock-core via metalavaGenerateSignature — purely additive (MockResponse gains a defaulted constructor parameter, a component7(), and a getter/property; MockResponse.Companion.create gains a defaulted parameter). docs/modules/networkmock-core.md updated to match (new "Schema-based response synthesis" section; the $ref resolution section already covered the reused ref machinery in #131).

Tests

  • SchemaSynthesizerTest.kt (new, 17 tests): each primitive type, enum, nested object (including the no-explicit-type-but-properties case), array (including the no-items error case), allOf merge (including the conflicting-members error case and the same-property-redeclared-identically non-conflict case), oneOf with and without discriminator (both falling back to the first variant), $ref resolution via the callback, and the no-recognizable-shape error case.
  • MockConfigRepositoryTest.kt (+3 end-to-end wiring tests): a schema-only status code synthesizes a body with isSynthesized = true; a status code with both examples and a schema prefers the examples (isSynthesized = false); a $ref'd schema under components.schemas resolves correctly before synthesizing.

Verification

.\gradlew.bat detektFull -Pandroidx.baselineprofile.skipgeneration
.\gradlew.bat testAndroidHostTest -Pandroidx.baselineprofile.skipgeneration
.\gradlew.bat :konsist:test -Pandroidx.baselineprofile.skipgeneration
.\gradlew.bat :devview-networkmock:compileAndroidDeviceTest -Pandroidx.baselineprofile.skipgeneration
.\gradlew.bat :sample:androidApp:assembleDebug -Pandroidx.baselineprofile.skipgeneration

All green, including a full repo-wide testAndroidHostTest run to confirm no fallout outside the networkmock modules.

🤖 Generated with GitHub Copilot

@MaxMichel2
MaxMichel2 force-pushed the feat/openapi-schema-synthesis branch from 3e7706b to 33185e4 Compare September 23, 2026 14:02
@MaxMichel2 MaxMichel2 changed the title ✨ feat: synthesise response bodies from OpenAPI schemas feat: synthesise response bodies from OpenAPI schemas Sep 23, 2026
@MaxMichel2
MaxMichel2 added this pull request to stack #130 September 23, 2026 14:02
@MaxMichel2
MaxMichel2 force-pushed the feat/openapi-schema-synthesis branch from 33185e4 to d91afc8 Compare September 23, 2026 14:11
@MaxMichel2 MaxMichel2 added this to the 0.2.0 milestone Sep 23, 2026
@MaxMichel2 MaxMichel2 self-assigned this Sep 23, 2026
@MaxMichel2
MaxMichel2 force-pushed the feat/openapi-schema-synthesis branch from d91afc8 to 7068c47 Compare October 1, 2026 09:38
A status code with a declared content.<mediaType>.schema but no examples
previously showed zero mockable variants. OpenApiParser now synthesizes one
placeholder body per such media type instead, via a new internal
SchemaSynthesizer: primitives, enum (first value), object/array
(recursively), allOf (properties merged, conflicting definitions across
members throw a clear error), and oneOf (first declared variant --
discriminator is parsed but doesn't yet steer variant selection, since
there's no concrete request/response data at spec-parse time to disambiguate
against). Deliberately narrow, not full JSON Schema conformance.

SchemaObject/DiscriminatorObject added to OpenApiDocument.kt following the
existing ref-capable-object style; ComponentsObject gains a schemas map;
MediaTypeObject gains a schema field. ResolvedResponse's file-path-only
content is generalised into a ResponseContent sealed interface (FromFile /
Synthesized) so a synthesized body can flow through the same response index
as a file-backed one. resolveResponseIndex's per-media-type example
resolution is extracted into resolveMediaTypeResponses to keep it under
detekt's LongMethod threshold.

MockResponse gains isSynthesized: Boolean (default false, additive); the
operation picker page shows a small "Generated" badge (MockItem.kt) on a
synthesized response's row, styled after EndpointCard's method badge.

api.txt regenerated for devview-networkmock-core; docs/modules/networkmock-core.md
gains a "Schema-based response synthesis" section.
SchemaSynthesizer took a (SchemaObject) -> SchemaObject callback that always
resolved against the media type's document, so a $ref nested inside a schema
that lives in an external file (e.g. User -> #/components/schemas/Address in
specs/shared/user.json) was looked up in the root spec's components -- failing
if absent, or silently picking a same-named root schema if present.

Replace the callback with an internal SchemaResolver that returns the resolved
schema together with the resolver scoped to the document it was found in, and
thread that through object/array/oneOf/allOf. allOf keeps a resolver per merged
property since members can come from different documents.

Add regression tests with a decoy root-level Address for the nested-ref and
cross-document allOf cases.
@MaxMichel2
MaxMichel2 force-pushed the feat/openapi-schema-synthesis branch from 7068c47 to 8fa2658 Compare October 2, 2026 14:27

This branch has not been deployed

No deployments
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.

1 participant