You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

491 lines
19 KiB

4 years ago
4 years ago
  1. #region License
  2. // Copyright (c) 2007 James Newton-King
  3. //
  4. // Permission is hereby granted, free of charge, to any person
  5. // obtaining a copy of this software and associated documentation
  6. // files (the "Software"), to deal in the Software without
  7. // restriction, including without limitation the rights to use,
  8. // copy, modify, merge, publish, distribute, sublicense, and/or sell
  9. // copies of the Software, and to permit persons to whom the
  10. // Software is furnished to do so, subject to the following
  11. // conditions:
  12. //
  13. // The above copyright notice and this permission notice shall be
  14. // included in all copies or substantial portions of the Software.
  15. //
  16. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  17. // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
  18. // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  19. // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
  20. // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
  21. // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  22. // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
  23. // OTHER DEALINGS IN THE SOFTWARE.
  24. #endregion
  25. using System;
  26. using System.Collections.Generic;
  27. using Newtonsoft.Json.Serialization;
  28. #if NET20
  29. using Newtonsoft.Json.Utilities.LinqBridge;
  30. #else
  31. using System.Linq;
  32. #endif
  33. using System.Globalization;
  34. using Newtonsoft.Json.Utilities;
  35. using Newtonsoft.Json.Linq;
  36. namespace Newtonsoft.Json.Schema
  37. {
  38. [Obsolete("JSON Schema validation has been moved to its own package. See http://www.newtonsoft.com/jsonschema for more details.")]
  39. internal class JsonSchemaBuilder
  40. {
  41. private readonly IList<JsonSchema> _stack;
  42. private readonly JsonSchemaResolver _resolver;
  43. private readonly IDictionary<string, JsonSchema> _documentSchemas;
  44. private JsonSchema _currentSchema;
  45. private JObject _rootSchema;
  46. public JsonSchemaBuilder(JsonSchemaResolver resolver)
  47. {
  48. _stack = new List<JsonSchema>();
  49. _documentSchemas = new Dictionary<string, JsonSchema>();
  50. _resolver = resolver;
  51. }
  52. private void Push(JsonSchema value)
  53. {
  54. _currentSchema = value;
  55. _stack.Add(value);
  56. _resolver.LoadedSchemas.Add(value);
  57. _documentSchemas.Add(value.Location, value);
  58. }
  59. private JsonSchema Pop()
  60. {
  61. JsonSchema poppedSchema = _currentSchema;
  62. _stack.RemoveAt(_stack.Count - 1);
  63. _currentSchema = _stack.LastOrDefault();
  64. return poppedSchema;
  65. }
  66. private JsonSchema CurrentSchema => _currentSchema;
  67. internal JsonSchema Read(JsonReader reader)
  68. {
  69. JToken schemaToken = JToken.ReadFrom(reader);
  70. _rootSchema = schemaToken as JObject;
  71. JsonSchema schema = BuildSchema(schemaToken);
  72. ResolveReferences(schema);
  73. return schema;
  74. }
  75. private string UnescapeReference(string reference)
  76. {
  77. return Uri.UnescapeDataString(reference).Replace("~1", "/").Replace("~0", "~");
  78. }
  79. private JsonSchema ResolveReferences(JsonSchema schema)
  80. {
  81. if (schema.DeferredReference != null)
  82. {
  83. string reference = schema.DeferredReference;
  84. bool locationReference = (reference.StartsWith("#", StringComparison.Ordinal));
  85. if (locationReference)
  86. {
  87. reference = UnescapeReference(reference);
  88. }
  89. JsonSchema resolvedSchema = _resolver.GetSchema(reference);
  90. if (resolvedSchema == null)
  91. {
  92. if (locationReference)
  93. {
  94. string[] escapedParts = schema.DeferredReference.TrimStart('#').Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
  95. JToken currentToken = _rootSchema;
  96. foreach (string escapedPart in escapedParts)
  97. {
  98. string part = UnescapeReference(escapedPart);
  99. if (currentToken.Type == JTokenType.Object)
  100. {
  101. currentToken = currentToken[part];
  102. }
  103. else if (currentToken.Type == JTokenType.Array || currentToken.Type == JTokenType.Constructor)
  104. {
  105. int index;
  106. if (int.TryParse(part, out index) && index >= 0 && index < currentToken.Count())
  107. {
  108. currentToken = currentToken[index];
  109. }
  110. else
  111. {
  112. currentToken = null;
  113. }
  114. }
  115. if (currentToken == null)
  116. {
  117. break;
  118. }
  119. }
  120. if (currentToken != null)
  121. {
  122. resolvedSchema = BuildSchema(currentToken);
  123. }
  124. }
  125. if (resolvedSchema == null)
  126. {
  127. throw new JsonException("Could not resolve schema reference '{0}'.".FormatWith(CultureInfo.InvariantCulture, schema.DeferredReference));
  128. }
  129. }
  130. schema = resolvedSchema;
  131. }
  132. if (schema.ReferencesResolved)
  133. {
  134. return schema;
  135. }
  136. schema.ReferencesResolved = true;
  137. if (schema.Extends != null)
  138. {
  139. for (int i = 0; i < schema.Extends.Count; i++)
  140. {
  141. schema.Extends[i] = ResolveReferences(schema.Extends[i]);
  142. }
  143. }
  144. if (schema.Items != null)
  145. {
  146. for (int i = 0; i < schema.Items.Count; i++)
  147. {
  148. schema.Items[i] = ResolveReferences(schema.Items[i]);
  149. }
  150. }
  151. if (schema.AdditionalItems != null)
  152. {
  153. schema.AdditionalItems = ResolveReferences(schema.AdditionalItems);
  154. }
  155. if (schema.PatternProperties != null)
  156. {
  157. foreach (KeyValuePair<string, JsonSchema> patternProperty in schema.PatternProperties.List())
  158. {
  159. schema.PatternProperties[patternProperty.Key] = ResolveReferences(patternProperty.Value);
  160. }
  161. }
  162. if (schema.Properties != null)
  163. {
  164. foreach (KeyValuePair<string, JsonSchema> property in schema.Properties.List())
  165. {
  166. schema.Properties[property.Key] = ResolveReferences(property.Value);
  167. }
  168. }
  169. if (schema.AdditionalProperties != null)
  170. {
  171. schema.AdditionalProperties = ResolveReferences(schema.AdditionalProperties);
  172. }
  173. return schema;
  174. }
  175. private JsonSchema BuildSchema(JToken token)
  176. {
  177. JObject schemaObject = token as JObject;
  178. if (schemaObject == null)
  179. {
  180. throw JsonException.Create(token, token.Path, "Expected object while parsing schema object, got {0}.".FormatWith(CultureInfo.InvariantCulture, token.Type));
  181. }
  182. JToken referenceToken;
  183. if (schemaObject.TryGetValue(JsonTypeReflector.RefPropertyName, out referenceToken))
  184. {
  185. JsonSchema deferredSchema = new JsonSchema();
  186. deferredSchema.DeferredReference = (string)referenceToken;
  187. return deferredSchema;
  188. }
  189. string location = token.Path.Replace(".", "/").Replace("[", "/").Replace("]", string.Empty);
  190. if (!string.IsNullOrEmpty(location))
  191. {
  192. location = "/" + location;
  193. }
  194. location = "#" + location;
  195. JsonSchema existingSchema;
  196. if (_documentSchemas.TryGetValue(location, out existingSchema))
  197. {
  198. return existingSchema;
  199. }
  200. Push(new JsonSchema { Location = location });
  201. ProcessSchemaProperties(schemaObject);
  202. return Pop();
  203. }
  204. private void ProcessSchemaProperties(JObject schemaObject)
  205. {
  206. foreach (KeyValuePair<string, JToken> property in schemaObject)
  207. {
  208. switch (property.Key)
  209. {
  210. case JsonSchemaConstants.TypePropertyName:
  211. CurrentSchema.Type = ProcessType(property.Value);
  212. break;
  213. case JsonSchemaConstants.IdPropertyName:
  214. CurrentSchema.Id = (string)property.Value;
  215. break;
  216. case JsonSchemaConstants.TitlePropertyName:
  217. CurrentSchema.Title = (string)property.Value;
  218. break;
  219. case JsonSchemaConstants.DescriptionPropertyName:
  220. CurrentSchema.Description = (string)property.Value;
  221. break;
  222. case JsonSchemaConstants.PropertiesPropertyName:
  223. CurrentSchema.Properties = ProcessProperties(property.Value);
  224. break;
  225. case JsonSchemaConstants.ItemsPropertyName:
  226. ProcessItems(property.Value);
  227. break;
  228. case JsonSchemaConstants.AdditionalPropertiesPropertyName:
  229. ProcessAdditionalProperties(property.Value);
  230. break;
  231. case JsonSchemaConstants.AdditionalItemsPropertyName:
  232. ProcessAdditionalItems(property.Value);
  233. break;
  234. case JsonSchemaConstants.PatternPropertiesPropertyName:
  235. CurrentSchema.PatternProperties = ProcessProperties(property.Value);
  236. break;
  237. case JsonSchemaConstants.RequiredPropertyName:
  238. CurrentSchema.Required = (bool)property.Value;
  239. break;
  240. case JsonSchemaConstants.RequiresPropertyName:
  241. CurrentSchema.Requires = (string)property.Value;
  242. break;
  243. case JsonSchemaConstants.MinimumPropertyName:
  244. CurrentSchema.Minimum = (double)property.Value;
  245. break;
  246. case JsonSchemaConstants.MaximumPropertyName:
  247. CurrentSchema.Maximum = (double)property.Value;
  248. break;
  249. case JsonSchemaConstants.ExclusiveMinimumPropertyName:
  250. CurrentSchema.ExclusiveMinimum = (bool)property.Value;
  251. break;
  252. case JsonSchemaConstants.ExclusiveMaximumPropertyName:
  253. CurrentSchema.ExclusiveMaximum = (bool)property.Value;
  254. break;
  255. case JsonSchemaConstants.MaximumLengthPropertyName:
  256. CurrentSchema.MaximumLength = (int)property.Value;
  257. break;
  258. case JsonSchemaConstants.MinimumLengthPropertyName:
  259. CurrentSchema.MinimumLength = (int)property.Value;
  260. break;
  261. case JsonSchemaConstants.MaximumItemsPropertyName:
  262. CurrentSchema.MaximumItems = (int)property.Value;
  263. break;
  264. case JsonSchemaConstants.MinimumItemsPropertyName:
  265. CurrentSchema.MinimumItems = (int)property.Value;
  266. break;
  267. case JsonSchemaConstants.DivisibleByPropertyName:
  268. CurrentSchema.DivisibleBy = (double)property.Value;
  269. break;
  270. case JsonSchemaConstants.DisallowPropertyName:
  271. CurrentSchema.Disallow = ProcessType(property.Value);
  272. break;
  273. case JsonSchemaConstants.DefaultPropertyName:
  274. CurrentSchema.Default = property.Value.DeepClone();
  275. break;
  276. case JsonSchemaConstants.HiddenPropertyName:
  277. CurrentSchema.Hidden = (bool)property.Value;
  278. break;
  279. case JsonSchemaConstants.ReadOnlyPropertyName:
  280. CurrentSchema.ReadOnly = (bool)property.Value;
  281. break;
  282. case JsonSchemaConstants.FormatPropertyName:
  283. CurrentSchema.Format = (string)property.Value;
  284. break;
  285. case JsonSchemaConstants.PatternPropertyName:
  286. CurrentSchema.Pattern = (string)property.Value;
  287. break;
  288. case JsonSchemaConstants.EnumPropertyName:
  289. ProcessEnum(property.Value);
  290. break;
  291. case JsonSchemaConstants.ExtendsPropertyName:
  292. ProcessExtends(property.Value);
  293. break;
  294. case JsonSchemaConstants.UniqueItemsPropertyName:
  295. CurrentSchema.UniqueItems = (bool)property.Value;
  296. break;
  297. }
  298. }
  299. }
  300. private void ProcessExtends(JToken token)
  301. {
  302. IList<JsonSchema> schemas = new List<JsonSchema>();
  303. if (token.Type == JTokenType.Array)
  304. {
  305. foreach (JToken schemaObject in token)
  306. {
  307. schemas.Add(BuildSchema(schemaObject));
  308. }
  309. }
  310. else
  311. {
  312. JsonSchema schema = BuildSchema(token);
  313. if (schema != null)
  314. {
  315. schemas.Add(schema);
  316. }
  317. }
  318. if (schemas.Count > 0)
  319. {
  320. CurrentSchema.Extends = schemas;
  321. }
  322. }
  323. private void ProcessEnum(JToken token)
  324. {
  325. if (token.Type != JTokenType.Array)
  326. {
  327. throw JsonException.Create(token, token.Path, "Expected Array token while parsing enum values, got {0}.".FormatWith(CultureInfo.InvariantCulture, token.Type));
  328. }
  329. CurrentSchema.Enum = new List<JToken>();
  330. foreach (JToken enumValue in token)
  331. {
  332. CurrentSchema.Enum.Add(enumValue.DeepClone());
  333. }
  334. }
  335. private void ProcessAdditionalProperties(JToken token)
  336. {
  337. if (token.Type == JTokenType.Boolean)
  338. {
  339. CurrentSchema.AllowAdditionalProperties = (bool)token;
  340. }
  341. else
  342. {
  343. CurrentSchema.AdditionalProperties = BuildSchema(token);
  344. }
  345. }
  346. private void ProcessAdditionalItems(JToken token)
  347. {
  348. if (token.Type == JTokenType.Boolean)
  349. {
  350. CurrentSchema.AllowAdditionalItems = (bool)token;
  351. }
  352. else
  353. {
  354. CurrentSchema.AdditionalItems = BuildSchema(token);
  355. }
  356. }
  357. private IDictionary<string, JsonSchema> ProcessProperties(JToken token)
  358. {
  359. IDictionary<string, JsonSchema> properties = new Dictionary<string, JsonSchema>();
  360. if (token.Type != JTokenType.Object)
  361. {
  362. throw JsonException.Create(token, token.Path, "Expected Object token while parsing schema properties, got {0}.".FormatWith(CultureInfo.InvariantCulture, token.Type));
  363. }
  364. foreach (JProperty propertyToken in token)
  365. {
  366. if (properties.ContainsKey(propertyToken.Name))
  367. {
  368. throw new JsonException("Property {0} has already been defined in schema.".FormatWith(CultureInfo.InvariantCulture, propertyToken.Name));
  369. }
  370. properties.Add(propertyToken.Name, BuildSchema(propertyToken.Value));
  371. }
  372. return properties;
  373. }
  374. private void ProcessItems(JToken token)
  375. {
  376. CurrentSchema.Items = new List<JsonSchema>();
  377. switch (token.Type)
  378. {
  379. case JTokenType.Object:
  380. CurrentSchema.Items.Add(BuildSchema(token));
  381. CurrentSchema.PositionalItemsValidation = false;
  382. break;
  383. case JTokenType.Array:
  384. CurrentSchema.PositionalItemsValidation = true;
  385. foreach (JToken schemaToken in token)
  386. {
  387. CurrentSchema.Items.Add(BuildSchema(schemaToken));
  388. }
  389. break;
  390. default:
  391. throw JsonException.Create(token, token.Path, "Expected array or JSON schema object, got {0}.".FormatWith(CultureInfo.InvariantCulture, token.Type));
  392. }
  393. }
  394. private JsonSchemaType? ProcessType(JToken token)
  395. {
  396. switch (token.Type)
  397. {
  398. case JTokenType.Array:
  399. // ensure type is in blank state before ORing values
  400. JsonSchemaType? type = JsonSchemaType.None;
  401. foreach (JToken typeToken in token)
  402. {
  403. if (typeToken.Type != JTokenType.String)
  404. {
  405. throw JsonException.Create(typeToken, typeToken.Path, "Expected JSON schema type string token, got {0}.".FormatWith(CultureInfo.InvariantCulture, token.Type));
  406. }
  407. type = type | MapType((string)typeToken);
  408. }
  409. return type;
  410. case JTokenType.String:
  411. return MapType((string)token);
  412. default:
  413. throw JsonException.Create(token, token.Path, "Expected array or JSON schema type string token, got {0}.".FormatWith(CultureInfo.InvariantCulture, token.Type));
  414. }
  415. }
  416. internal static JsonSchemaType MapType(string type)
  417. {
  418. JsonSchemaType mappedType;
  419. if (!JsonSchemaConstants.JsonSchemaTypeMapping.TryGetValue(type, out mappedType))
  420. {
  421. throw new JsonException("Invalid JSON schema type: {0}".FormatWith(CultureInfo.InvariantCulture, type));
  422. }
  423. return mappedType;
  424. }
  425. internal static string MapType(JsonSchemaType type)
  426. {
  427. return JsonSchemaConstants.JsonSchemaTypeMapping.Single(kv => kv.Value == type).Key;
  428. }
  429. }
  430. }