From 32326f3d80ca9e92af2286acbf8f235740268d9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Thu, 9 Apr 2026 20:43:49 +0200 Subject: [PATCH 1/3] feat(core): emit warnings when unknown schemas are mapped to JsonElement (#37) After parsing, scan the model for TypeRef.Unknown occurrences and emit a warning for each one, including the schema/property or endpoint context. This surfaces silent type-safety losses so users can fix their spec. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../justworks/core/parser/SpecParser.kt | 48 ++++++++++++++++++- .../justworks/core/parser/SpecParserTest.kt | 43 +++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/core/src/main/kotlin/com/avsystem/justworks/core/parser/SpecParser.kt b/core/src/main/kotlin/com/avsystem/justworks/core/parser/SpecParser.kt index 131443e..1068207 100644 --- a/core/src/main/kotlin/com/avsystem/justworks/core/parser/SpecParser.kt +++ b/core/src/main/kotlin/com/avsystem/justworks/core/parser/SpecParser.kt @@ -14,6 +14,7 @@ import arrow.core.toNonEmptyListOrNull import com.avsystem.justworks.core.Issue import com.avsystem.justworks.core.SCHEMA_PREFIX import com.avsystem.justworks.core.Warnings +import com.avsystem.justworks.core.ensureOrAccumulate import com.avsystem.justworks.core.model.ApiSpec import com.avsystem.justworks.core.model.ContentType import com.avsystem.justworks.core.model.Discriminator @@ -160,11 +161,15 @@ object SpecParser { } val syntheticModels = collectModels(emptySet(), emptyList()) + + val allModels = schemaModels + syntheticModels + warnOnUnknownTypes(endpoints, allModels) + return ApiSpec( title = info?.title ?: "Untitled", version = info?.version ?: "0.0.0", endpoints = endpoints, - schemas = schemaModels + syntheticModels, + schemas = allModels, enums = enumModels, ) } @@ -443,6 +448,47 @@ object SpecParser { ) } + context(_: Warnings) + private fun warnOnUnknownTypes(endpoints: List, schemas: List) { + fun warn(context: String) { + ensureOrAccumulate(false) { + Issue.Warning("$context: unresolvable type mapped to JsonElement") + } + } + + for (schema in schemas) { + for (prop in schema.properties) { + if (containsUnknown(prop.type)) { + warn("Schema '${schema.name}', property '${prop.name}'") + } + } + } + for (endpoint in endpoints) { + val op = endpoint.operationId + for ((code, response) in endpoint.responses) { + if (response.schema != null && containsUnknown(response.schema)) { + warn("Endpoint '$op', response '$code'") + } + } + if (endpoint.requestBody != null && containsUnknown(endpoint.requestBody.schema)) { + warn("Endpoint '$op', request body") + } + for (param in endpoint.parameters) { + if (containsUnknown(param.schema)) { + warn("Endpoint '$op', parameter '${param.name}'") + } + } + } + } + + private fun containsUnknown(type: TypeRef): Boolean = when (type) { + TypeRef.Unknown -> true + is TypeRef.Array -> containsUnknown(type.items) + is TypeRef.Map -> containsUnknown(type.valueType) + is TypeRef.Inline -> type.properties.any { containsUnknown(it.type) } + is TypeRef.Primitive, is TypeRef.Reference -> false + } + private fun generateOperationId(method: HttpMethod, path: String): String { val segments = path .split("/") diff --git a/core/src/test/kotlin/com/avsystem/justworks/core/parser/SpecParserTest.kt b/core/src/test/kotlin/com/avsystem/justworks/core/parser/SpecParserTest.kt index e531d94..e6828a1 100644 --- a/core/src/test/kotlin/com/avsystem/justworks/core/parser/SpecParserTest.kt +++ b/core/src/test/kotlin/com/avsystem/justworks/core/parser/SpecParserTest.kt @@ -160,6 +160,49 @@ class SpecParserTest : SpecParserTestBase() { assertEquals("Pet", itemType.schemaName) } + // -- SPEC-01b: Warnings for unknown schemas -- + + @Test + fun `parse spec with unresolvable schema emits warning`() { + val result = SpecParser.parse( + """ + openapi: 3.0.0 + info: + title: Test + version: 1.0.0 + paths: {} + components: + schemas: + Container: + type: object + properties: + data: + type: object + """.trimIndent().toTempFile(), + ) + assertIs(result) + val warningMessages = result.warnings.map { it.message } + assertTrue( + warningMessages.any { + it.contains("Container") && it.contains("data") && it.contains("JsonElement") + }, + "Expected warning about unresolvable type, got: $warningMessages", + ) + } + + @Test + fun `parse spec without unknown schemas has no unknown-type warnings`() { + val result = SpecParser.parse(loadResource("petstore.yaml")) + assertIs(result) + val unknownWarnings = result.warnings.filter { + it.message.contains("JsonElement") + } + assertTrue( + unknownWarnings.isEmpty(), + "Petstore should have no unknown-type warnings, got: $unknownWarnings", + ) + } + // -- SPEC-02: $ref resolution -- @Test From a45bfc9db1ac139e61cabb85f75754281a7e893f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Fri, 10 Apr 2026 09:47:28 +0200 Subject: [PATCH 2/3] fix(core): add override modifier to HttpError.message property Generated HttpError extends RuntimeException but its `message` property was missing the `override` modifier, causing compilation failures in downstream projects. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../avsystem/justworks/core/gen/shared/ApiResponseGenerator.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/main/kotlin/com/avsystem/justworks/core/gen/shared/ApiResponseGenerator.kt b/core/src/main/kotlin/com/avsystem/justworks/core/gen/shared/ApiResponseGenerator.kt index 202ea8c..02e5543 100644 --- a/core/src/main/kotlin/com/avsystem/justworks/core/gen/shared/ApiResponseGenerator.kt +++ b/core/src/main/kotlin/com/avsystem/justworks/core/gen/shared/ApiResponseGenerator.kt @@ -57,6 +57,7 @@ internal object ApiResponseGenerator { ).addProperty( PropertySpec .builder(MESSAGE, STRING) + .addModifiers(KModifier.OVERRIDE) .initializer(MESSAGE) .build(), ).addProperty( From bcee6858b8f764410afeb43b28512726c9e6fd47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Fri, 10 Apr 2026 15:19:12 +0200 Subject: [PATCH 3/3] cleanup --- .../justworks/core/parser/SpecParser.kt | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/core/src/main/kotlin/com/avsystem/justworks/core/parser/SpecParser.kt b/core/src/main/kotlin/com/avsystem/justworks/core/parser/SpecParser.kt index 19cd9b7..446555a 100644 --- a/core/src/main/kotlin/com/avsystem/justworks/core/parser/SpecParser.kt +++ b/core/src/main/kotlin/com/avsystem/justworks/core/parser/SpecParser.kt @@ -187,15 +187,15 @@ object SpecParser { } val syntheticModels = collectModels(emptySet(), emptyList()) + val schemas = schemaModels + syntheticModels - val allModels = schemaModels + syntheticModels - warnOnUnknownTypes(endpoints, allModels) + warnOnUnknownTypes(endpoints, schemas) return ApiSpec( title = title, version = info?.version ?: "0.0.0", endpoints = endpoints, - schemas = allModels, + schemas = schemas, enums = enumModels, securitySchemes = securitySchemes, ) @@ -513,32 +513,35 @@ object SpecParser { context(_: Warnings) private fun warnOnUnknownTypes(endpoints: List, schemas: List) { - fun warn(context: String) { - ensureOrAccumulate(false) { - Issue.Warning("$context: unresolvable type mapped to JsonElement") - } - } - for (schema in schemas) { for (prop in schema.properties) { - if (containsUnknown(prop.type)) { - warn("Schema '${schema.name}', property '${prop.name}'") + ensureOrAccumulate(!containsUnknown(prop.type)) { + Issue.Warning( + "Schema '${schema.name}', property '${prop.name}': unresolvable type mapped to JsonElement", + ) + } + } + if (schema.underlyingType != null) { + ensureOrAccumulate(!containsUnknown(schema.underlyingType)) { + Issue.Warning( + "Schema '${schema.name}': underlying type contains unresolvable type mapped to JsonElement", + ) } } } for (endpoint in endpoints) { val op = endpoint.operationId for ((code, response) in endpoint.responses) { - if (response.schema != null && containsUnknown(response.schema)) { - warn("Endpoint '$op', response '$code'") + ensureOrAccumulate(response.schema == null || !containsUnknown(response.schema)) { + Issue.Warning("Endpoint '$op', response '$code': unresolvable type mapped to JsonElement") } } - if (endpoint.requestBody != null && containsUnknown(endpoint.requestBody.schema)) { - warn("Endpoint '$op', request body") + ensureOrAccumulate(endpoint.requestBody == null || !containsUnknown(endpoint.requestBody.schema)) { + Issue.Warning("Endpoint '$op', request body: unresolvable type mapped to JsonElement") } for (param in endpoint.parameters) { - if (containsUnknown(param.schema)) { - warn("Endpoint '$op', parameter '${param.name}'") + ensureOrAccumulate(!containsUnknown(param.schema)) { + Issue.Warning("Endpoint '$op', parameter '${param.name}': unresolvable type mapped to JsonElement") } } }