Skip to content

Commit 544126f

Browse files
authored
Merge pull request #2765 from hongwei1/obp-develop
bugfix/fix nested array schema generation for GeoJSON MultiPolygon coordinates
2 parents e39e13e + 9983080 commit 544126f

3 files changed

Lines changed: 568 additions & 0 deletions

File tree

obp-api/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,15 @@ object JSONFactory1_4_0 extends MdcLoggable{
655655
}
656656
}
657657

658+
// Helper function to detect nested arrays (JArray containing JArray)
659+
def isNestedArray(value: Any): Boolean = value match {
660+
case JArray(List(f, _*)) => f match {
661+
case _: JArray => true
662+
case _ => false
663+
}
664+
case _ => false
665+
}
666+
658667
//please check issue first: https://github.com/OpenBankProject/OBP-API/issues/877
659668
//change:
660669
// { "first_name": "George"} --> {"type": "object","properties": {"first_name": {"type": "string" }
@@ -672,10 +681,37 @@ object JSONFactory1_4_0 extends MdcLoggable{
672681
case v => v
673682
}
674683

684+
// Early return for JArray - handle both nested arrays and primitive arrays
685+
// This prevents JArray's internal "arr" field from being extracted by reflection
686+
extractedEntity match {
687+
case JArray(List(f, _*)) if isNestedArray(extractedEntity) =>
688+
// Nested array: recursively generate nested array schema
689+
val innerSchema = translateEntity(f, false)
690+
return """{"type": "array", "items": """ + innerSchema + "}"
691+
case JArray(List(f, _*)) =>
692+
// Non-nested array: generate array schema with primitive or object items
693+
val itemType = f match {
694+
case _: JInt => """{"type": "integer"}"""
695+
case _: JDouble => """{"type": "number"}"""
696+
case _: JBool => """{"type": "boolean"}"""
697+
case _: JString => """{"type": "string"}"""
698+
case _: JArray =>
699+
// This is a nested array - recursively handle it
700+
translateEntity(f, false)
701+
case _ => translateEntity(f, false) // For objects or other complex types
702+
}
703+
return """{"type": "array", "items": """ + itemType + "}"
704+
case JArray(List()) =>
705+
// Empty array
706+
return """{"type": "array"}"""
707+
case _ => // Continue with normal processing
708+
}
709+
675710
val mapOfFields: Map[String, Any] = extractedEntity match {
676711

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

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

756792
//List case classes.
793+
// Handle nested arrays (JArray containing JArray) - generate pure nested array schema
794+
case JArray(List(f,_*)) if f.isInstanceOf[JArray] =>
795+
// For nested arrays, recursively generate nested array schema
796+
// The recursive call will handle further nesting
797+
val innerSchema = translateEntity(f, false)
798+
"\"" + key + """": {"type": "array", "items": """ + innerSchema + "}"
757799
case JArray(List(f,_*)) => "\"" + key + """":""" +translateEntity(f,true)
758800
case List(f) => "\"" + key + """":""" +translateEntity(f,true)
759801
case List(f,_*) => "\"" + key + """":""" +translateEntity(f,true)
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
package code.api.v1_4_0
2+
3+
import code.api.util.CustomJsonFormats
4+
import code.util.Helper.MdcLoggable
5+
import net.liftweb.json._
6+
import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, FeatureSpec, GivenWhenThen, Matchers}
7+
8+
/**
9+
* Bug Condition Exploration Test for Nested Array Schema Generation
10+
*
11+
* CRITICAL: This test MUST FAIL on unfixed code - failure confirms the bug exists
12+
* DO NOT attempt to fix the test or the code when it fails
13+
*
14+
* This test encodes the expected behavior - it will validate the fix when it passes after implementation
15+
* GOAL: Surface counterexamples that demonstrate the bug exists
16+
*
17+
* Bug Condition: When translateEntity encounters a JArray containing another JArray,
18+
* it incorrectly generates nested objects with "arr" properties instead of proper nested array schema.
19+
*
20+
* Expected Behavior: Nested arrays should generate {"type": "array", "items": {"type": "array", ...}}
21+
* without object wrappers.
22+
*/
23+
class JSONFactory1_4_0NestedArrayTest extends FeatureSpec
24+
with BeforeAndAfterEach
25+
with GivenWhenThen
26+
with BeforeAndAfterAll
27+
with Matchers
28+
with MdcLoggable
29+
with CustomJsonFormats {
30+
31+
feature("Bug Condition: Nested Array Schema Generation") {
32+
33+
scenario("2-level nested array should generate correct nested array schema") {
34+
Given("A 2-level nested JArray: JArray(List(JArray(List(JInt(42)))))")
35+
val nestedArray = JArray(List(JArray(List(JInt(42)))))
36+
val testObject = JObject(List(JField("coordinates", nestedArray)))
37+
38+
When("translateEntity is called on the nested array")
39+
val schema = JSONFactory1_4_0.translateEntity(testObject, false)
40+
41+
Then("The schema should contain nested array types without object wrappers")
42+
logger.info(s"Generated schema for 2-level nested array: {$schema}")
43+
44+
// Expected: {"type": "array", "items": {"type": "array", "items": {"type": "integer"}}}
45+
// Current (buggy): Contains "type": "object" with "properties": {"arr": ...}
46+
47+
// Check that schema does NOT contain the buggy pattern with "arr" property
48+
schema should not include """"arr":"""
49+
50+
// Check that schema contains proper nested array structure
51+
schema should include (""""type": "array"""")
52+
53+
// Parse the schema to verify structure
54+
val parsedSchema = parse(schema)
55+
56+
val coordinatesField = (parsedSchema \ "properties" \ "coordinates")
57+
(coordinatesField \ "type").extract[String] shouldBe "array"
58+
59+
val itemsLevel1 = (coordinatesField \ "items")
60+
(itemsLevel1 \ "type").extract[String] shouldBe "array"
61+
62+
// Should NOT contain "properties" with "arr" key
63+
(itemsLevel1 \ "properties" \ "arr") shouldBe JNothing
64+
65+
val itemsLevel2 = (itemsLevel1 \ "items")
66+
(itemsLevel2 \ "type").extract[String] shouldBe "integer"
67+
}
68+
69+
scenario("3-level nested array should generate correct nested array schema") {
70+
Given("A 3-level nested JArray: JArray(List(JArray(List(JArray(List(JString('value')))))))")
71+
val nestedArray = JArray(List(JArray(List(JArray(List(JString("value")))))))
72+
val testObject = JObject(List(JField("data", nestedArray)))
73+
74+
When("translateEntity is called on the nested array")
75+
val schema = JSONFactory1_4_0.translateEntity(testObject, false)
76+
77+
Then("The schema should contain 3 levels of nested array types")
78+
logger.info(s"Generated schema for 3-level nested array: {$schema}")
79+
80+
// Check that schema does NOT contain the buggy pattern with "arr" property
81+
schema should not include """"arr":"""
82+
83+
val parsedSchema = parse(schema)
84+
85+
val dataField = (parsedSchema \ "properties" \ "data")
86+
(dataField \ "type").extract[String] shouldBe "array"
87+
88+
val itemsLevel1 = (dataField \ "items")
89+
(itemsLevel1 \ "type").extract[String] shouldBe "array"
90+
(itemsLevel1 \ "properties" \ "arr") shouldBe JNothing
91+
92+
val itemsLevel2 = (itemsLevel1 \ "items")
93+
(itemsLevel2 \ "type").extract[String] shouldBe "array"
94+
(itemsLevel2 \ "properties" \ "arr") shouldBe JNothing
95+
96+
val itemsLevel3 = (itemsLevel2 \ "items")
97+
(itemsLevel3 \ "type").extract[String] shouldBe "string"
98+
}
99+
100+
scenario("4-level GeoJSON MultiPolygon coordinates should generate correct nested array schema") {
101+
Given("A 4-level nested JArray representing GeoJSON MultiPolygon coordinates")
102+
val coordinates = JArray(List(
103+
JArray(List(
104+
JArray(List(
105+
JArray(List(JDouble(102.0), JDouble(2.0))),
106+
JArray(List(JDouble(103.0), JDouble(2.0))),
107+
JArray(List(JDouble(103.0), JDouble(3.0))),
108+
JArray(List(JDouble(102.0), JDouble(3.0))),
109+
JArray(List(JDouble(102.0), JDouble(2.0)))
110+
))
111+
))
112+
))
113+
val testObject = JObject(List(JField("coordinates", coordinates)))
114+
115+
When("translateEntity is called on the GeoJSON coordinates")
116+
val schema = JSONFactory1_4_0.translateEntity(testObject, false)
117+
118+
Then("The schema should contain 4 levels of nested array types terminating in number")
119+
logger.info(s"Generated schema for GeoJSON MultiPolygon: {$schema}")
120+
121+
// Check that schema does NOT contain the buggy pattern with "arr" property
122+
schema should not include """"arr":"""
123+
124+
val parsedSchema = parse(schema)
125+
126+
val coordinatesField = (parsedSchema \ "properties" \ "coordinates")
127+
(coordinatesField \ "type").extract[String] shouldBe "array"
128+
129+
val itemsLevel1 = (coordinatesField \ "items")
130+
(itemsLevel1 \ "type").extract[String] shouldBe "array"
131+
(itemsLevel1 \ "properties" \ "arr") shouldBe JNothing
132+
133+
val itemsLevel2 = (itemsLevel1 \ "items")
134+
(itemsLevel2 \ "type").extract[String] shouldBe "array"
135+
(itemsLevel2 \ "properties" \ "arr") shouldBe JNothing
136+
137+
val itemsLevel3 = (itemsLevel2 \ "items")
138+
(itemsLevel3 \ "type").extract[String] shouldBe "array"
139+
(itemsLevel3 \ "properties" \ "arr") shouldBe JNothing
140+
141+
val itemsLevel4 = (itemsLevel3 \ "items")
142+
(itemsLevel4 \ "type").extract[String] shouldBe "number"
143+
}
144+
145+
scenario("Empty nested array should be handled gracefully") {
146+
Given("An empty nested JArray: JArray(List(JArray(List())))")
147+
val emptyNestedArray = JArray(List(JArray(List())))
148+
val testObject = JObject(List(JField("empty", emptyNestedArray)))
149+
150+
When("translateEntity is called on the empty nested array")
151+
val schema = JSONFactory1_4_0.translateEntity(testObject, false)
152+
153+
Then("The schema should handle the empty nested array gracefully")
154+
logger.debug(s"Generated schema for empty nested array: $schema")
155+
156+
val parsedSchema = parse(schema)
157+
158+
val emptyField = (parsedSchema \ "properties" \ "empty")
159+
(emptyField \ "type").extract[String] shouldBe "array"
160+
161+
val itemsLevel1 = (emptyField \ "items")
162+
(itemsLevel1 \ "type").extract[String] shouldBe "array"
163+
164+
// Should NOT contain "properties" with "arr" key
165+
(itemsLevel1 \ "properties" \ "arr") shouldBe JNothing
166+
}
167+
}
168+
}

0 commit comments

Comments
 (0)