@@ -1838,4 +1838,279 @@ describe('ValidationService', () => {
18381838 expect ( result ) . to . be . undefined ;
18391839 } ) ;
18401840 } ) ;
1841+
1842+ describe ( 'Circular reference handling' , ( ) => {
1843+ it ( 'should handle self-referencing refAlias without stack overflow' , ( ) => {
1844+ const models : TsoaRoute . Models = {
1845+ RecursiveType : {
1846+ dataType : 'refAlias' ,
1847+ type : { ref : 'RecursiveType' } ,
1848+ } ,
1849+ } ;
1850+
1851+ const v = new ValidationService ( models , {
1852+ noImplicitAdditionalProperties : 'ignore' ,
1853+ bodyCoercion : true ,
1854+ } ) ;
1855+
1856+ const fieldErrors : FieldErrors = { } ;
1857+ const value = { some : 'data' } ;
1858+
1859+ const result = v . validateModel ( {
1860+ name : '' ,
1861+ value,
1862+ modelDefinition : models . RecursiveType ,
1863+ fieldErrors,
1864+ isBodyParam : true ,
1865+ } ) ;
1866+
1867+ expect ( result ) . to . deep . equal ( value ) ;
1868+ } ) ;
1869+
1870+ it ( 'should handle deeply nested circular references' , ( ) => {
1871+ const models : TsoaRoute . Models = {
1872+ Widget : {
1873+ dataType : 'refAlias' ,
1874+ type : {
1875+ dataType : 'union' ,
1876+ subSchemas : [ { ref : 'Container' } , { ref : 'Wrapper' } ] ,
1877+ } ,
1878+ } ,
1879+ Container : {
1880+ dataType : 'refObject' ,
1881+ properties : {
1882+ type : { dataType : 'string' , required : true } ,
1883+ items : {
1884+ dataType : 'array' ,
1885+ array : { ref : 'Widget' } ,
1886+ required : true ,
1887+ } ,
1888+ } ,
1889+ } ,
1890+ Wrapper : {
1891+ dataType : 'refObject' ,
1892+ properties : {
1893+ type : { dataType : 'string' , required : true } ,
1894+ content : {
1895+ ref : 'Widget' ,
1896+ required : true ,
1897+ } ,
1898+ } ,
1899+ } ,
1900+ } ;
1901+
1902+ const v = new ValidationService ( models , {
1903+ noImplicitAdditionalProperties : 'ignore' ,
1904+ bodyCoercion : true ,
1905+ } ) ;
1906+
1907+ const fieldErrors : FieldErrors = { } ;
1908+
1909+ const value = {
1910+ type : 'container' ,
1911+ items : [
1912+ {
1913+ type : 'wrapper' ,
1914+ content : {
1915+ type : 'nested-container' ,
1916+ items : [
1917+ {
1918+ type : 'deeply-nested-wrapper' ,
1919+ content : {
1920+ type : 'deeply-nested-container' ,
1921+ items : [ ] ,
1922+ } ,
1923+ } ,
1924+ ] ,
1925+ } ,
1926+ } ,
1927+ ] ,
1928+ } ;
1929+
1930+ const result = v . validateModel ( {
1931+ name : '' ,
1932+ value,
1933+ modelDefinition : models . Container ,
1934+ fieldErrors,
1935+ isBodyParam : true ,
1936+ } ) ;
1937+
1938+ expect ( result ) . to . exist ;
1939+ expect ( result . type ) . to . equal ( 'container' ) ;
1940+ } ) ;
1941+
1942+ it ( 'should handle circular references with arrays' , ( ) => {
1943+ const models : TsoaRoute . Models = {
1944+ Node : {
1945+ dataType : 'refObject' ,
1946+ properties : {
1947+ id : { dataType : 'string' , required : true } ,
1948+ children : {
1949+ dataType : 'array' ,
1950+ array : { ref : 'Node' } ,
1951+ required : false ,
1952+ } ,
1953+ } ,
1954+ } ,
1955+ } ;
1956+
1957+ const v = new ValidationService ( models , {
1958+ noImplicitAdditionalProperties : 'ignore' ,
1959+ bodyCoercion : true ,
1960+ } ) ;
1961+
1962+ const fieldErrors : FieldErrors = { } ;
1963+
1964+ const value = {
1965+ id : 'root' ,
1966+ children : [
1967+ {
1968+ id : 'child1' ,
1969+ children : [
1970+ {
1971+ id : 'grandchild1' ,
1972+ } ,
1973+ ] ,
1974+ } ,
1975+ {
1976+ id : 'child2' ,
1977+ } ,
1978+ ] ,
1979+ } ;
1980+
1981+ const result = v . validateModel ( {
1982+ name : '' ,
1983+ value,
1984+ modelDefinition : models . Node ,
1985+ fieldErrors,
1986+ isBodyParam : true ,
1987+ } ) ;
1988+
1989+ expect ( result ) . to . exist ;
1990+ expect ( result . id ) . to . equal ( 'root' ) ;
1991+ expect ( result . children ) . to . have . lengthOf ( 2 ) ;
1992+ } ) ;
1993+
1994+ it ( 'should properly fail validation for invalid data in circular types' , ( ) => {
1995+ const models : TsoaRoute . Models = {
1996+ Node : {
1997+ dataType : 'refObject' ,
1998+ properties : {
1999+ id : { dataType : 'string' , required : true } ,
2000+ value : { dataType : 'integer' , required : true } ,
2001+ children : {
2002+ dataType : 'array' ,
2003+ array : { ref : 'Node' } ,
2004+ required : false ,
2005+ } ,
2006+ } ,
2007+ } ,
2008+ } ;
2009+
2010+ const v = new ValidationService ( models , {
2011+ noImplicitAdditionalProperties : 'throw-on-extras' ,
2012+ bodyCoercion : true ,
2013+ } ) ;
2014+
2015+ const fieldErrors : FieldErrors = { } ;
2016+
2017+ const value = {
2018+ id : 'root' ,
2019+ value : 1 ,
2020+ children : [
2021+ {
2022+ value : 2 ,
2023+ children : [
2024+ {
2025+ id : 'grandchild1' ,
2026+ value : 3 ,
2027+ } ,
2028+ ] ,
2029+ } ,
2030+ {
2031+ id : 'child2' ,
2032+ value : 'not-a-number' ,
2033+ } ,
2034+ ] ,
2035+ } ;
2036+
2037+ const result = v . validateModel ( {
2038+ name : '' ,
2039+ value,
2040+ modelDefinition : models . Node ,
2041+ fieldErrors,
2042+ isBodyParam : true ,
2043+ } ) ;
2044+
2045+ expect ( Object . keys ( fieldErrors ) ) . to . not . be . empty ;
2046+ expect ( fieldErrors ) . to . have . property ( 'children.$0.id' ) ;
2047+ expect ( fieldErrors [ 'children.$0.id' ] . message ) . to . include ( 'required' ) ;
2048+ expect ( fieldErrors ) . to . have . property ( 'children.$1.value' ) ;
2049+ expect ( fieldErrors [ 'children.$1.value' ] . message ) . to . include ( 'integer' ) ;
2050+ expect ( result ) . to . be . undefined ;
2051+ } ) ;
2052+
2053+ it ( 'should detect validation errors in deeply nested circular structures' , ( ) => {
2054+ const models : TsoaRoute . Models = {
2055+ Widget : {
2056+ dataType : 'refAlias' ,
2057+ type : {
2058+ dataType : 'union' ,
2059+ subSchemas : [ { ref : 'Container' } , { ref : 'Wrapper' } ] ,
2060+ } ,
2061+ } ,
2062+ Container : {
2063+ dataType : 'refObject' ,
2064+ properties : {
2065+ type : { dataType : 'string' , required : true } ,
2066+ items : {
2067+ dataType : 'array' ,
2068+ array : { ref : 'Widget' } ,
2069+ required : true ,
2070+ } ,
2071+ } ,
2072+ } ,
2073+ Wrapper : {
2074+ dataType : 'refObject' ,
2075+ properties : {
2076+ type : { dataType : 'string' , required : true } ,
2077+ content : {
2078+ ref : 'Widget' ,
2079+ required : true ,
2080+ } ,
2081+ } ,
2082+ } ,
2083+ } ;
2084+
2085+ const v = new ValidationService ( models , {
2086+ noImplicitAdditionalProperties : 'throw-on-extras' ,
2087+ bodyCoercion : true ,
2088+ } ) ;
2089+
2090+ const fieldErrors : FieldErrors = { } ;
2091+
2092+ const value = {
2093+ type : 'container' ,
2094+ items : [
2095+ {
2096+ content : {
2097+ type : 'nested-container' ,
2098+ items : [ ] ,
2099+ } ,
2100+ } ,
2101+ ] ,
2102+ } ;
2103+
2104+ const result = v . validateModel ( {
2105+ name : '' ,
2106+ value,
2107+ modelDefinition : models . Container ,
2108+ fieldErrors,
2109+ isBodyParam : true ,
2110+ } ) ;
2111+
2112+ expect ( Object . keys ( fieldErrors ) ) . to . not . be . empty ;
2113+ expect ( result ) . to . be . undefined ;
2114+ } ) ;
2115+ } ) ;
18412116} ) ;
0 commit comments