Skip to content

feat: synthesise response bodies from OpenAPI schemas - #132

Open
MaxMichel2 wants to merge 1 commit into
fix/openapi-ref-resolutionfrom
feat/openapi-schema-synthesis
Open

MaxMichel2 wants to merge 1 commit 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
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.

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