1+ #nullable enable
2+
3+ using System ;
4+ using System . Collections ;
5+ using System . Collections . Generic ;
6+ using System . Linq ;
7+ using NeuroSdk . Actions ;
8+ using Newtonsoft . Json . Linq ;
9+
10+ namespace NeuroSdk . Json
11+ {
12+ public class JsonSchemaValidator
13+ {
14+ /// <summary>
15+ /// Validate an object (POCO, Dictionary, etc.) against a JsonSchema
16+ /// Returns false and the error message if invalid
17+ /// </summary>
18+ public static bool ValidateSafe ( JsonSchema schema , ActionJData ? obj , out string ? message , string ? path = "" )
19+ {
20+ return ValidateSafe ( schema , ( object ? ) obj ? . Data , out message , path ) ;
21+ }
22+
23+ /// <summary>
24+ /// Validate an object (POCO, Dictionary, etc.) against a JsonSchema
25+ /// Returns false and the error message if invalid
26+ /// </summary>
27+ public static bool ValidateSafe ( JsonSchema schema , JToken ? obj , out string ? message , string ? path = "" )
28+ {
29+ return ValidateSafe ( schema , ( object ? ) obj , out message , path ) ;
30+ }
31+
32+ /// <summary>
33+ /// Validate an object (POCO, Dictionary, etc.) against a JsonSchema
34+ /// Returns false and the error message if invalid
35+ /// </summary>
36+ public static bool ValidateSafe ( JsonSchema schema , object ? obj , out string ? message , string ? path = "" )
37+ {
38+ try
39+ {
40+ Validate ( schema , obj ) ;
41+ }
42+ catch ( Exception e )
43+ {
44+ message = e . Message ;
45+ return false ;
46+ }
47+
48+ message = null ;
49+ return true ;
50+ }
51+
52+ /// <summary>
53+ /// Validate an object (POCO, Dictionary, etc.) against a JsonSchema
54+ /// Throws an exception if invalid
55+ /// </summary>
56+ public static void Validate ( JsonSchema schema , ActionJData ? actionData , string path = "" )
57+ {
58+ if ( actionData == null )
59+ {
60+ throw new Exception ( $ "{ path } : expected action data") ;
61+ }
62+
63+ var token = actionData . Data ;
64+
65+ if ( token == null || token . Type == JTokenType . Null )
66+ {
67+ if ( schema . Type == JsonSchemaType . Null || schema . Const == null ) return ;
68+ throw new Exception ( $ "{ path } : value is null but schema does not allow null") ;
69+ }
70+
71+ object ? obj = token . Type switch
72+ {
73+ JTokenType . Object => token . ToObject < Dictionary < string , object > > ( ) ! ,
74+ JTokenType . Array => token . ToObject < List < object > > ( ) ! ,
75+ JTokenType . Integer => token . Value < long > ( ) ,
76+ JTokenType . Float => token . Value < double > ( ) ,
77+ JTokenType . Boolean => token . Value < bool > ( ) ,
78+ JTokenType . String => token . Value < string > ( ) ,
79+ _ => throw new Exception ( $ "{ path } : unsupported token type { token . Type } ")
80+ } ;
81+
82+ Validate ( schema , obj , path ) ;
83+ }
84+
85+ /// <summary>
86+ /// Validate an object (POCO, Dictionary, etc.) against a JsonSchema
87+ /// Throws an exception if invalid
88+ /// </summary>
89+ public static void Validate ( JsonSchema schema , JToken ? token , string path = "" )
90+ {
91+ if ( token == null || token . Type == JTokenType . Null )
92+ {
93+ if ( schema . Type == JsonSchemaType . Null /* || schema.Const == null */ ) return ;
94+ throw new Exception ( $ "{ path } : value is null but schema does not allow null") ;
95+ }
96+
97+ object ? obj = token . Type switch
98+ {
99+ JTokenType . Object => token . ToObject < Dictionary < string , object > > ( ) ! ,
100+ JTokenType . Array => token . ToObject < List < object > > ( ) ! ,
101+ JTokenType . Integer => token . Value < long > ( ) ,
102+ JTokenType . Float => token . Value < double > ( ) ,
103+ JTokenType . Boolean => token . Value < bool > ( ) ,
104+ JTokenType . String => token . Value < string > ( ) ,
105+ _ => throw new Exception ( $ "{ path } : unsupported token type { token . Type } ")
106+ } ;
107+
108+ Validate ( schema , obj , path ) ;
109+ }
110+
111+ /// <summary>
112+ /// Validate an object (POCO, Dictionary, etc.) against a JsonSchema
113+ /// Throws an exception if invalid
114+ /// </summary>
115+ public static void Validate ( JsonSchema schema , object ? obj , string path = "" )
116+ {
117+ if ( schema == null ) throw new ArgumentNullException ( nameof ( schema ) ) ;
118+ if ( obj == null )
119+ {
120+ // How do I check if the schema.Const is explicitly null?
121+ // Welp, I just won't check for that I guess.
122+ if ( schema . Type == JsonSchemaType . Null /* || schema.Const == null */ ) return ;
123+ throw new Exception ( $ "{ path } : value is null but schema does not allow null") ;
124+ }
125+
126+ switch ( schema . Type )
127+ {
128+ case JsonSchemaType . String :
129+ ValidateString ( schema , obj , path ) ;
130+ break ;
131+ case JsonSchemaType . Float :
132+ ValidateFloat ( schema , obj , path ) ;
133+ break ;
134+ case JsonSchemaType . Integer :
135+ ValidateInteger ( schema , obj , path ) ;
136+ break ;
137+ case JsonSchemaType . Object :
138+ ValidateObject ( schema , obj , path ) ;
139+ break ;
140+ case JsonSchemaType . Array :
141+ ValidateArray ( schema , obj , path ) ;
142+ break ;
143+ case JsonSchemaType . Boolean :
144+ ValidateBoolean ( obj , path ) ;
145+ break ;
146+ case JsonSchemaType . Null :
147+ ValidateNull ( obj , path ) ;
148+ break ;
149+ case JsonSchemaType . None :
150+ break ;
151+ default :
152+ throw new ArgumentOutOfRangeException ( ) ;
153+ }
154+
155+
156+ if ( schema . Const != null && ! schema . Const . Equals ( obj ) )
157+ throw new Exception ( $ "{ path } : value must be constant { schema . Const } ") ;
158+
159+ if ( schema . Enum != null && schema . Enum . Count > 0 && ! schema . Enum . Contains ( obj ) )
160+ throw new Exception ( $ "{ path } : value must be one of [{ string . Join ( ", " , schema . Enum ) } ]") ;
161+ }
162+
163+ private static void ValidateString ( JsonSchema schema , object obj , string path )
164+ {
165+ if ( obj is not string s )
166+ throw new Exception ( $ "{ path } : expected string") ;
167+
168+ if ( schema . MinLength . HasValue && s . Length < schema . MinLength . Value )
169+ throw new Exception ( $ "{ path } : string too short (min { schema . MinLength . Value } )") ;
170+
171+ if ( schema . MaxLength . HasValue && s . Length > schema . MaxLength . Value )
172+ throw new Exception ( $ "{ path } : string too long (max { schema . MaxLength . Value } )") ;
173+
174+ if ( string . IsNullOrEmpty ( schema . Pattern ) ) return ;
175+
176+ if ( schema . Pattern != null && ! System . Text . RegularExpressions . Regex . IsMatch ( s , schema . Pattern ) )
177+ throw new Exception ( $ "{ path } : string does not match pattern { schema . Pattern } ") ;
178+ }
179+
180+ private static void ValidateFloat ( JsonSchema schema , object obj , string path )
181+ {
182+ switch ( obj )
183+ {
184+ case float f :
185+ ValidateNumber ( schema , f , path ) ;
186+ break ;
187+ case double d :
188+ ValidateNumber ( schema , d , path ) ;
189+ break ;
190+ case int i :
191+ ValidateNumber ( schema , i , path ) ;
192+ break ;
193+ default :
194+ throw new Exception ( $ "{ path } : expected float") ;
195+ }
196+ }
197+
198+ private static void ValidateInteger ( JsonSchema schema , object obj , string path )
199+ {
200+ switch ( obj )
201+ {
202+ case int i :
203+ ValidateNumber ( schema , i , path ) ;
204+ break ;
205+ case long l :
206+ ValidateNumber ( schema , l , path ) ;
207+ break ;
208+ default :
209+ throw new Exception ( $ "{ path } : expected integer") ;
210+ }
211+ }
212+
213+ private static void ValidateNumber ( JsonSchema schema , double value , string path )
214+ {
215+ if ( schema . Minimum . HasValue && value < schema . Minimum . Value )
216+ throw new Exception ( $ "{ path } : value { value } < minimum { schema . Minimum . Value } ") ;
217+ if ( schema . Maximum . HasValue && value > schema . Maximum . Value )
218+ throw new Exception ( $ "{ path } : value { value } > maximum { schema . Maximum . Value } ") ;
219+ if ( schema . ExclusiveMinimum . HasValue && value <= schema . ExclusiveMinimum . Value )
220+ throw new Exception ( $ "{ path } : value { value } <= exclusive minimum { schema . ExclusiveMinimum . Value } ") ;
221+ if ( schema . ExclusiveMaximum . HasValue && value >= schema . ExclusiveMaximum . Value )
222+ throw new Exception ( $ "{ path } : value { value } >= exclusive maximum { schema . ExclusiveMaximum . Value } ") ;
223+ }
224+
225+ private static void ValidateObject ( JsonSchema schema , object obj , string path )
226+ {
227+
228+ if ( obj is not IDictionary < string , object > dict )
229+ {
230+ if ( obj is JObject jObj )
231+ dict = jObj . ToObject < Dictionary < string , object > > ( ) ! ;
232+ else
233+ throw new Exception ( $ "{ path } : expected object") ;
234+ }
235+
236+ foreach ( var req in schema . Required . Where ( req => ! dict . ContainsKey ( req ) ) )
237+ throw new Exception ( $ "{ MakePath ( path , req ) } : missing required property") ;
238+
239+ foreach ( var kvp in dict )
240+ {
241+ if ( ! schema . Properties . TryGetValue ( kvp . Key , out var subSchema ) )
242+ {
243+ if ( ! schema . AllowAdditionalProperties )
244+ throw new Exception ( $ "{ MakePath ( path , kvp . Key ) } : unknown property not allowed") ;
245+ }
246+ else
247+ {
248+ Validate ( subSchema , kvp . Value , $ "{ MakePath ( path , kvp . Key ) } ") ;
249+ }
250+ }
251+
252+ return ;
253+
254+ string MakePath ( string parentPath , string key )
255+ {
256+ if ( string . IsNullOrEmpty ( parentPath ) )
257+ return key ;
258+ return parentPath + "." + key ;
259+ }
260+ }
261+
262+ private static void ValidateArray ( JsonSchema schema , object obj , string path )
263+ {
264+ if ( obj is not IEnumerable enumerable )
265+ throw new Exception ( $ "{ path } : expected array") ;
266+
267+ var list = enumerable . Cast < object > ( ) . ToList ( ) ;
268+
269+ if ( schema . MinItems . HasValue && list . Count < schema . MinItems . Value )
270+ throw new Exception (
271+ $ "{ path } : array item count must be at least { schema . MinItems . Value } "
272+ ) ;
273+
274+ if ( schema . MaxItems . HasValue && list . Count > schema . MaxItems . Value )
275+ throw new Exception (
276+ $ "{ path } : array item count must be at most { schema . MaxItems . Value } "
277+ ) ;
278+
279+ if ( schema . UniqueItems == true && list . Distinct ( ) . Count ( ) != list . Count )
280+ throw new Exception (
281+ $ "{ path } : array items must be unique"
282+ ) ;
283+
284+
285+ if ( schema . Items == null ) return ;
286+
287+ for ( var i = 0 ; i < list . Count ; i ++ )
288+ Validate ( schema . Items , list [ i ] , $ "{ path } [{ i } ]") ;
289+ }
290+
291+ private static void ValidateBoolean ( object obj , string path )
292+ {
293+ if ( obj is not bool )
294+ throw new Exception ( $ "{ path } : expected boolean, got { obj . GetType ( ) . Name } ") ;
295+ }
296+
297+ private static void ValidateNull ( object obj , string path )
298+ {
299+ if ( obj is not null ) throw new Exception ( $ "{ path } : expected null") ;
300+ }
301+ }
302+ }
0 commit comments