Skip to content

Commit 349a3a1

Browse files
committed
moved validators elsewhere
1 parent 00ea0b2 commit 349a3a1

File tree

3 files changed

+305
-289
lines changed

3 files changed

+305
-289
lines changed
Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
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+
}

Unity/Assets/Json/JsonSchemaValidator.cs.meta

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)