Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions obp-api/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,15 @@ object JSONFactory1_4_0 extends MdcLoggable{
}
}

// Helper function to detect nested arrays (JArray containing JArray)
def isNestedArray(value: Any): Boolean = value match {
case JArray(List(f, _*)) => f match {
case _: JArray => true
case _ => false
}
case _ => false
}

//please check issue first: https://github.com/OpenBankProject/OBP-API/issues/877
//change:
// { "first_name": "George"} --> {"type": "object","properties": {"first_name": {"type": "string" }
Expand All @@ -672,10 +681,37 @@ object JSONFactory1_4_0 extends MdcLoggable{
case v => v
}

// Early return for JArray - handle both nested arrays and primitive arrays
// This prevents JArray's internal "arr" field from being extracted by reflection
extractedEntity match {
case JArray(List(f, _*)) if isNestedArray(extractedEntity) =>
// Nested array: recursively generate nested array schema
val innerSchema = translateEntity(f, false)
return """{"type": "array", "items": """ + innerSchema + "}"
case JArray(List(f, _*)) =>
// Non-nested array: generate array schema with primitive or object items
val itemType = f match {
case _: JInt => """{"type": "integer"}"""
case _: JDouble => """{"type": "number"}"""
case _: JBool => """{"type": "boolean"}"""
case _: JString => """{"type": "string"}"""
case _: JArray =>
// This is a nested array - recursively handle it
translateEntity(f, false)
case _ => translateEntity(f, false) // For objects or other complex types
}
return """{"type": "array", "items": """ + itemType + "}"
case JArray(List()) =>
// Empty array
return """{"type": "array"}"""
case _ => // Continue with normal processing
}

val mapOfFields: Map[String, Any] = extractedEntity match {

case ListResult(name, results) => Map((name, results))
case JObject(jFields) => jFields.map(it => (it.name, it.value)).toMap
case _: JArray => Map.empty // Don't extract fields from JArray - it has internal "arr" field
case _ => ReflectUtils.getFieldValues(extractedEntity.asInstanceOf[AnyRef])()
}

Expand Down Expand Up @@ -754,6 +790,12 @@ object JSONFactory1_4_0 extends MdcLoggable{
case Some(List(i: BigDecimal, _*)) => "\"" + key + """": {"type": "array","items": {"type": "number"}}"""

//List case classes.
// Handle nested arrays (JArray containing JArray) - generate pure nested array schema
case JArray(List(f,_*)) if f.isInstanceOf[JArray] =>
// For nested arrays, recursively generate nested array schema
// The recursive call will handle further nesting
val innerSchema = translateEntity(f, false)
"\"" + key + """": {"type": "array", "items": """ + innerSchema + "}"
case JArray(List(f,_*)) => "\"" + key + """":""" +translateEntity(f,true)
case List(f) => "\"" + key + """":""" +translateEntity(f,true)
case List(f,_*) => "\"" + key + """":""" +translateEntity(f,true)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package code.api.v1_4_0

import code.api.util.CustomJsonFormats
import code.util.Helper.MdcLoggable
import net.liftweb.json._
import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, FeatureSpec, GivenWhenThen, Matchers}

/**
* Bug Condition Exploration Test for Nested Array Schema Generation
*
* CRITICAL: This test MUST FAIL on unfixed code - failure confirms the bug exists
* DO NOT attempt to fix the test or the code when it fails
*
* This test encodes the expected behavior - it will validate the fix when it passes after implementation
* GOAL: Surface counterexamples that demonstrate the bug exists
*
* Bug Condition: When translateEntity encounters a JArray containing another JArray,
* it incorrectly generates nested objects with "arr" properties instead of proper nested array schema.
*
* Expected Behavior: Nested arrays should generate {"type": "array", "items": {"type": "array", ...}}
* without object wrappers.
*/
class JSONFactory1_4_0NestedArrayTest extends FeatureSpec

Check warning on line 23 in obp-api/src/test/scala/code/api/v1_4_0/JSONFactory1_4_0NestedArrayTest.scala

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename class "JSONFactory1_4_0NestedArrayTest" to match the regular expression ^[A-Z][a-zA-Z0-9]*$.

See more on https://sonarcloud.io/project/issues?id=OpenBankProject_OBP-API&issues=AZ23PnNl43vI1cIAWgbV&open=AZ23PnNl43vI1cIAWgbV&pullRequest=2765
with BeforeAndAfterEach
with GivenWhenThen
with BeforeAndAfterAll
with Matchers
with MdcLoggable
with CustomJsonFormats {

feature("Bug Condition: Nested Array Schema Generation") {

scenario("2-level nested array should generate correct nested array schema") {
Given("A 2-level nested JArray: JArray(List(JArray(List(JInt(42)))))")
val nestedArray = JArray(List(JArray(List(JInt(42)))))
val testObject = JObject(List(JField("coordinates", nestedArray)))

When("translateEntity is called on the nested array")
val schema = JSONFactory1_4_0.translateEntity(testObject, false)

Then("The schema should contain nested array types without object wrappers")
logger.info(s"Generated schema for 2-level nested array: {$schema}")

// Expected: {"type": "array", "items": {"type": "array", "items": {"type": "integer"}}}
// Current (buggy): Contains "type": "object" with "properties": {"arr": ...}

// Check that schema does NOT contain the buggy pattern with "arr" property
schema should not include """"arr":"""

Check failure on line 48 in obp-api/src/test/scala/code/api/v1_4_0/JSONFactory1_4_0NestedArrayTest.scala

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal ""arr":" 3 times.

See more on https://sonarcloud.io/project/issues?id=OpenBankProject_OBP-API&issues=AZ23PnNl43vI1cIAWgbU&open=AZ23PnNl43vI1cIAWgbU&pullRequest=2765

// Check that schema contains proper nested array structure
schema should include (""""type": "array"""")

// Parse the schema to verify structure
val parsedSchema = parse(schema)

val coordinatesField = (parsedSchema \ "properties" \ "coordinates")
(coordinatesField \ "type").extract[String] shouldBe "array"

val itemsLevel1 = (coordinatesField \ "items")
(itemsLevel1 \ "type").extract[String] shouldBe "array"

// Should NOT contain "properties" with "arr" key
(itemsLevel1 \ "properties" \ "arr") shouldBe JNothing

val itemsLevel2 = (itemsLevel1 \ "items")
(itemsLevel2 \ "type").extract[String] shouldBe "integer"
}

scenario("3-level nested array should generate correct nested array schema") {
Given("A 3-level nested JArray: JArray(List(JArray(List(JArray(List(JString('value')))))))")
val nestedArray = JArray(List(JArray(List(JArray(List(JString("value")))))))
val testObject = JObject(List(JField("data", nestedArray)))

When("translateEntity is called on the nested array")
val schema = JSONFactory1_4_0.translateEntity(testObject, false)

Then("The schema should contain 3 levels of nested array types")
logger.info(s"Generated schema for 3-level nested array: {$schema}")

// Check that schema does NOT contain the buggy pattern with "arr" property
schema should not include """"arr":"""

val parsedSchema = parse(schema)

val dataField = (parsedSchema \ "properties" \ "data")
(dataField \ "type").extract[String] shouldBe "array"

val itemsLevel1 = (dataField \ "items")
(itemsLevel1 \ "type").extract[String] shouldBe "array"
(itemsLevel1 \ "properties" \ "arr") shouldBe JNothing

val itemsLevel2 = (itemsLevel1 \ "items")
(itemsLevel2 \ "type").extract[String] shouldBe "array"
(itemsLevel2 \ "properties" \ "arr") shouldBe JNothing

val itemsLevel3 = (itemsLevel2 \ "items")
(itemsLevel3 \ "type").extract[String] shouldBe "string"
}

scenario("4-level GeoJSON MultiPolygon coordinates should generate correct nested array schema") {
Given("A 4-level nested JArray representing GeoJSON MultiPolygon coordinates")
val coordinates = JArray(List(
JArray(List(
JArray(List(
JArray(List(JDouble(102.0), JDouble(2.0))),
JArray(List(JDouble(103.0), JDouble(2.0))),
JArray(List(JDouble(103.0), JDouble(3.0))),
JArray(List(JDouble(102.0), JDouble(3.0))),
JArray(List(JDouble(102.0), JDouble(2.0)))
))
))
))
val testObject = JObject(List(JField("coordinates", coordinates)))

When("translateEntity is called on the GeoJSON coordinates")
val schema = JSONFactory1_4_0.translateEntity(testObject, false)

Then("The schema should contain 4 levels of nested array types terminating in number")
logger.info(s"Generated schema for GeoJSON MultiPolygon: {$schema}")

// Check that schema does NOT contain the buggy pattern with "arr" property
schema should not include """"arr":"""

val parsedSchema = parse(schema)

val coordinatesField = (parsedSchema \ "properties" \ "coordinates")
(coordinatesField \ "type").extract[String] shouldBe "array"

val itemsLevel1 = (coordinatesField \ "items")
(itemsLevel1 \ "type").extract[String] shouldBe "array"
(itemsLevel1 \ "properties" \ "arr") shouldBe JNothing

val itemsLevel2 = (itemsLevel1 \ "items")
(itemsLevel2 \ "type").extract[String] shouldBe "array"
(itemsLevel2 \ "properties" \ "arr") shouldBe JNothing

val itemsLevel3 = (itemsLevel2 \ "items")
(itemsLevel3 \ "type").extract[String] shouldBe "array"
(itemsLevel3 \ "properties" \ "arr") shouldBe JNothing

val itemsLevel4 = (itemsLevel3 \ "items")
(itemsLevel4 \ "type").extract[String] shouldBe "number"
}

scenario("Empty nested array should be handled gracefully") {
Given("An empty nested JArray: JArray(List(JArray(List())))")
val emptyNestedArray = JArray(List(JArray(List())))
val testObject = JObject(List(JField("empty", emptyNestedArray)))

When("translateEntity is called on the empty nested array")
val schema = JSONFactory1_4_0.translateEntity(testObject, false)

Then("The schema should handle the empty nested array gracefully")
logger.debug(s"Generated schema for empty nested array: $schema")

val parsedSchema = parse(schema)

val emptyField = (parsedSchema \ "properties" \ "empty")
(emptyField \ "type").extract[String] shouldBe "array"

val itemsLevel1 = (emptyField \ "items")
(itemsLevel1 \ "type").extract[String] shouldBe "array"

// Should NOT contain "properties" with "arr" key
(itemsLevel1 \ "properties" \ "arr") shouldBe JNothing
}
}
}
Loading
Loading