Handling non escaped strings in swagger generator

This commit is contained in:
simonredfern 2025-12-22 06:08:02 +01:00
parent 00490b95ed
commit 72ad27d2b8
2 changed files with 114 additions and 7 deletions

View File

@ -40,6 +40,35 @@ import scala.reflect.runtime.universe
object SwaggerJSONFactory extends MdcLoggable {
type Coll[T] = GenTraversableLike[T, _]
/**
* Escapes a string value to be safely included in JSON.
* Handles quotes, backslashes, newlines, and other special characters.
*/
private def escapeJsonString(value: String): String = {
if (value == null) return ""
value
.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t")
.replace("\b", "\\b")
.replace("\f", "\\f")
}
/**
* Safely converts any value to a JSON example string.
* Handles JValue, String, and other types with proper escaping.
*/
private def safeExampleValue(value: Any): String = {
value match {
case null | None => ""
case v: JValue => try { escapeJsonString(JsonUtils.toString(v)) } catch { case e: Exception => logger.warn(s"Failed to convert JValue to string for example: ${e.getMessage}"); "" }
case v: String => escapeJsonString(v)
case v => escapeJsonString(v.toString)
}
}
//Info Object
//link ->https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#infoObject
case class InfoJson(
@ -107,14 +136,26 @@ object SwaggerJSONFactory extends MdcLoggable {
| }
|}
|""".stripMargin
json.parse(definition)
try {
json.parse(definition)
} catch {
case e: Exception =>
logger.error(s"Failed to parse ListResult schema JSON: ${e.getMessage}\nJSON was: $definition")
throw new RuntimeException(s"Invalid JSON in ListResult schema generation: ${e.getMessage}", e)
}
}
}
case class JObjectSchemaJson(jObject: JObject) extends ResponseObjectSchemaJson with JsonAble {
override def toJValue(implicit format: Formats): json.JValue = {
val schema = buildSwaggerSchema(typeOf[JObject], jObject)
json.parse(schema)
try {
json.parse(schema)
} catch {
case e: Exception =>
logger.error(s"Failed to parse JObject schema JSON: ${e.getMessage}\nSchema was: $schema")
throw new RuntimeException(s"Invalid JSON in JObject schema generation: ${e.getMessage}", e)
}
}
}
@ -122,7 +163,13 @@ object SwaggerJSONFactory extends MdcLoggable {
override def toJValue(implicit format: Formats): json.JValue = {
val schema = buildSwaggerSchema(typeOf[JArray], jArray)
json.parse(schema)
try {
json.parse(schema)
} catch {
case e: Exception =>
logger.error(s"Failed to parse JArray schema JSON: ${e.getMessage}\nSchema was: $schema")
throw new RuntimeException(s"Invalid JSON in JArray schema generation: ${e.getMessage}", e)
}
}
}
@ -646,8 +693,7 @@ object SwaggerJSONFactory extends MdcLoggable {
}
def example = exampleValue match {
case null | None => ""
case v: JValue => s""", "example": "${JsonUtils.toString(v)}" """
case v => s""", "example": "$v" """
case v => s""", "example": "${safeExampleValue(v)}" """
}
paramType match {
@ -968,11 +1014,12 @@ object SwaggerJSONFactory extends MdcLoggable {
.toList
.map(it => {
val (errorName, errorMessage) = it
val escapedMessage = escapeJsonString(errorMessage.toString)
s""""Error$errorName": {
| "properties": {
| "message": {
| "type": "string",
| "example": "$errorMessage"
| "example": "$escapedMessage"
| }
| }
}""".stripMargin
@ -989,7 +1036,14 @@ object SwaggerJSONFactory extends MdcLoggable {
//Make a final string
val definitions = "{\"definitions\":{" + particularDefinitionsPart + "}}"
//Make a jsonAST from a string
parse(definitions)
try {
parse(definitions)
} catch {
case e: Exception =>
logger.error(s"Failed to parse Swagger definitions JSON: ${e.getMessage}")
logger.error(s"JSON was: ${definitions.take(500)}...")
throw new RuntimeException(s"Invalid JSON in Swagger definitions generation. This may be due to unescaped special characters in examples or field names. Error: ${e.getMessage}", e)
}
}

View File

@ -99,4 +99,57 @@ class SwaggerFactoryUnitTest extends V140ServerSetup with MdcLoggable {
}
}
feature("Test JSON escaping robustness in Swagger generation") {
scenario("Test quotes in example values are properly escaped") {
case class TestWithQuotes(name: String, description: String)
val testObj = TestWithQuotes(name = "Test with \"quotes\"", description = "Has 'single' and \"double\" quotes")
val result = SwaggerJSONFactory.translateEntity(testObj)
noException should be thrownBy { net.liftweb.json.parse("{" + result + "}") }
result should include ("\\\"")
}
scenario("Test newlines and special chars are properly escaped") {
case class TestWithNewlines(text: String)
val testObj = TestWithNewlines(text = "Line 1\nLine 2\tTab")
val result = SwaggerJSONFactory.translateEntity(testObj)
noException should be thrownBy { net.liftweb.json.parse("{" + result + "}") }
result should include ("\\n")
}
scenario("Test ABAC rule-like strings with escaped quotes") {
case class AbacRule(rule: String)
val testObj = AbacRule(rule = """user.emailAddress.contains(\"admin\")""")
val result = SwaggerJSONFactory.translateEntity(testObj)
noException should be thrownBy { net.liftweb.json.parse("{" + result + "}") }
}
scenario("Test error messages with special characters") {
import code.api.v1_4_0.JSONFactory1_4_0
val mockResourceDoc = JSONFactory1_4_0.ResourceDocJson(
operation_id = "testOp",
implemented_by = JSONFactory1_4_0.ImplementedByJson("1.0.0", "test"),
request_verb = "GET",
request_url = "/test",
summary = "Test",
description = "Test desc",
description_markdown = "Test desc",
example_request_body = null,
success_response_body = SwaggerDefinitionsJSON.bankJSON,
error_response_bodies = List("OBP-10000"),
tags = List("Test"),
typed_request_body = net.liftweb.json.JNothing,
typed_success_response_body = net.liftweb.json.JNothing,
roles = Some(List()),
is_featured = false,
special_instructions = "",
specified_url = "/obp/v4.0.0/test",
connector_methods = List(),
created_by_bank_id = None
)
noException should be thrownBy {
SwaggerJSONFactory.loadDefinitions(List(mockResourceDoc), SwaggerDefinitionsJSON.allFields.take(10))
}
}
}
}