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.

499 lines
20 KiB

  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.Globalization;
  27. using System.ComponentModel;
  28. using System.Collections.Generic;
  29. using Newtonsoft.Json.Linq;
  30. using Newtonsoft.Json.Utilities;
  31. using Newtonsoft.Json.Serialization;
  32. #if NET20
  33. using Newtonsoft.Json.Utilities.LinqBridge;
  34. #else
  35. using System.Linq;
  36. #endif
  37. namespace Newtonsoft.Json.Schema
  38. {
  39. /// <summary>
  40. /// <para>
  41. /// Generates a <see cref="JsonSchema"/> from a specified <see cref="Type"/>.
  42. /// </para>
  43. /// <note type="caution">
  44. /// JSON Schema validation has been moved to its own package. See <see href="http://www.newtonsoft.com/jsonschema">http://www.newtonsoft.com/jsonschema</see> for more details.
  45. /// </note>
  46. /// </summary>
  47. [Obsolete("JSON Schema validation has been moved to its own package. See http://www.newtonsoft.com/jsonschema for more details.")]
  48. internal class JsonSchemaGenerator
  49. {
  50. /// <summary>
  51. /// Gets or sets how undefined schemas are handled by the serializer.
  52. /// </summary>
  53. public UndefinedSchemaIdHandling UndefinedSchemaIdHandling { get; set; }
  54. private IContractResolver _contractResolver;
  55. /// <summary>
  56. /// Gets or sets the contract resolver.
  57. /// </summary>
  58. /// <value>The contract resolver.</value>
  59. public IContractResolver ContractResolver
  60. {
  61. get
  62. {
  63. if (_contractResolver == null)
  64. {
  65. return DefaultContractResolver.Instance;
  66. }
  67. return _contractResolver;
  68. }
  69. set => _contractResolver = value;
  70. }
  71. private class TypeSchema
  72. {
  73. public Type Type { get; }
  74. public JsonSchema Schema { get; }
  75. public TypeSchema(Type type, JsonSchema schema)
  76. {
  77. ValidationUtils.ArgumentNotNull(type, nameof(type));
  78. ValidationUtils.ArgumentNotNull(schema, nameof(schema));
  79. Type = type;
  80. Schema = schema;
  81. }
  82. }
  83. private JsonSchemaResolver _resolver;
  84. private readonly IList<TypeSchema> _stack = new List<TypeSchema>();
  85. private JsonSchema _currentSchema;
  86. private JsonSchema CurrentSchema => _currentSchema;
  87. private void Push(TypeSchema typeSchema)
  88. {
  89. _currentSchema = typeSchema.Schema;
  90. _stack.Add(typeSchema);
  91. _resolver.LoadedSchemas.Add(typeSchema.Schema);
  92. }
  93. private TypeSchema Pop()
  94. {
  95. TypeSchema popped = _stack[_stack.Count - 1];
  96. _stack.RemoveAt(_stack.Count - 1);
  97. TypeSchema newValue = _stack.LastOrDefault();
  98. if (newValue != null)
  99. {
  100. _currentSchema = newValue.Schema;
  101. }
  102. else
  103. {
  104. _currentSchema = null;
  105. }
  106. return popped;
  107. }
  108. /// <summary>
  109. /// Generate a <see cref="JsonSchema"/> from the specified type.
  110. /// </summary>
  111. /// <param name="type">The type to generate a <see cref="JsonSchema"/> from.</param>
  112. /// <returns>A <see cref="JsonSchema"/> generated from the specified type.</returns>
  113. public JsonSchema Generate(Type type)
  114. {
  115. return Generate(type, new JsonSchemaResolver(), false);
  116. }
  117. /// <summary>
  118. /// Generate a <see cref="JsonSchema"/> from the specified type.
  119. /// </summary>
  120. /// <param name="type">The type to generate a <see cref="JsonSchema"/> from.</param>
  121. /// <param name="resolver">The <see cref="JsonSchemaResolver"/> used to resolve schema references.</param>
  122. /// <returns>A <see cref="JsonSchema"/> generated from the specified type.</returns>
  123. public JsonSchema Generate(Type type, JsonSchemaResolver resolver)
  124. {
  125. return Generate(type, resolver, false);
  126. }
  127. /// <summary>
  128. /// Generate a <see cref="JsonSchema"/> from the specified type.
  129. /// </summary>
  130. /// <param name="type">The type to generate a <see cref="JsonSchema"/> from.</param>
  131. /// <param name="rootSchemaNullable">Specify whether the generated root <see cref="JsonSchema"/> will be nullable.</param>
  132. /// <returns>A <see cref="JsonSchema"/> generated from the specified type.</returns>
  133. public JsonSchema Generate(Type type, bool rootSchemaNullable)
  134. {
  135. return Generate(type, new JsonSchemaResolver(), rootSchemaNullable);
  136. }
  137. /// <summary>
  138. /// Generate a <see cref="JsonSchema"/> from the specified type.
  139. /// </summary>
  140. /// <param name="type">The type to generate a <see cref="JsonSchema"/> from.</param>
  141. /// <param name="resolver">The <see cref="JsonSchemaResolver"/> used to resolve schema references.</param>
  142. /// <param name="rootSchemaNullable">Specify whether the generated root <see cref="JsonSchema"/> will be nullable.</param>
  143. /// <returns>A <see cref="JsonSchema"/> generated from the specified type.</returns>
  144. public JsonSchema Generate(Type type, JsonSchemaResolver resolver, bool rootSchemaNullable)
  145. {
  146. ValidationUtils.ArgumentNotNull(type, nameof(type));
  147. ValidationUtils.ArgumentNotNull(resolver, nameof(resolver));
  148. _resolver = resolver;
  149. return GenerateInternal(type, (!rootSchemaNullable) ? Required.Always : Required.Default, false);
  150. }
  151. private string GetTitle(Type type)
  152. {
  153. JsonContainerAttribute containerAttribute = JsonTypeReflector.GetCachedAttribute<JsonContainerAttribute>(type);
  154. if (!string.IsNullOrEmpty(containerAttribute?.Title))
  155. {
  156. return containerAttribute.Title;
  157. }
  158. return null;
  159. }
  160. private string GetDescription(Type type)
  161. {
  162. JsonContainerAttribute containerAttribute = JsonTypeReflector.GetCachedAttribute<JsonContainerAttribute>(type);
  163. if (!string.IsNullOrEmpty(containerAttribute?.Description))
  164. {
  165. return containerAttribute.Description;
  166. }
  167. #if HAVE_ADO_NET
  168. DescriptionAttribute descriptionAttribute = ReflectionUtils.GetAttribute<DescriptionAttribute>(type);
  169. return descriptionAttribute?.Description;
  170. #else
  171. return null;
  172. #endif
  173. }
  174. private string GetTypeId(Type type, bool explicitOnly)
  175. {
  176. JsonContainerAttribute containerAttribute = JsonTypeReflector.GetCachedAttribute<JsonContainerAttribute>(type);
  177. if (!string.IsNullOrEmpty(containerAttribute?.Id))
  178. {
  179. return containerAttribute.Id;
  180. }
  181. if (explicitOnly)
  182. {
  183. return null;
  184. }
  185. switch (UndefinedSchemaIdHandling)
  186. {
  187. case UndefinedSchemaIdHandling.UseTypeName:
  188. return type.FullName;
  189. case UndefinedSchemaIdHandling.UseAssemblyQualifiedName:
  190. return type.AssemblyQualifiedName;
  191. default:
  192. return null;
  193. }
  194. }
  195. private JsonSchema GenerateInternal(Type type, Required valueRequired, bool required)
  196. {
  197. ValidationUtils.ArgumentNotNull(type, nameof(type));
  198. string resolvedId = GetTypeId(type, false);
  199. string explicitId = GetTypeId(type, true);
  200. if (!string.IsNullOrEmpty(resolvedId))
  201. {
  202. JsonSchema resolvedSchema = _resolver.GetSchema(resolvedId);
  203. if (resolvedSchema != null)
  204. {
  205. // resolved schema is not null but referencing member allows nulls
  206. // change resolved schema to allow nulls. hacky but what are ya gonna do?
  207. if (valueRequired != Required.Always && !HasFlag(resolvedSchema.Type, JsonSchemaType.Null))
  208. {
  209. resolvedSchema.Type |= JsonSchemaType.Null;
  210. }
  211. if (required && resolvedSchema.Required != true)
  212. {
  213. resolvedSchema.Required = true;
  214. }
  215. return resolvedSchema;
  216. }
  217. }
  218. // test for unresolved circular reference
  219. if (_stack.Any(tc => tc.Type == type))
  220. {
  221. throw new JsonException("Unresolved circular reference for type '{0}'. Explicitly define an Id for the type using a JsonObject/JsonArray attribute or automatically generate a type Id using the UndefinedSchemaIdHandling property.".FormatWith(CultureInfo.InvariantCulture, type));
  222. }
  223. JsonContract contract = ContractResolver.ResolveContract(type);
  224. JsonConverter converter = contract.Converter ?? contract.InternalConverter;
  225. Push(new TypeSchema(type, new JsonSchema()));
  226. if (explicitId != null)
  227. {
  228. CurrentSchema.Id = explicitId;
  229. }
  230. if (required)
  231. {
  232. CurrentSchema.Required = true;
  233. }
  234. CurrentSchema.Title = GetTitle(type);
  235. CurrentSchema.Description = GetDescription(type);
  236. if (converter != null)
  237. {
  238. // todo: Add GetSchema to JsonConverter and use here?
  239. CurrentSchema.Type = JsonSchemaType.Any;
  240. }
  241. else
  242. {
  243. switch (contract.ContractType)
  244. {
  245. case JsonContractType.Object:
  246. CurrentSchema.Type = AddNullType(JsonSchemaType.Object, valueRequired);
  247. CurrentSchema.Id = GetTypeId(type, false);
  248. GenerateObjectSchema(type, (JsonObjectContract)contract);
  249. break;
  250. case JsonContractType.Array:
  251. CurrentSchema.Type = AddNullType(JsonSchemaType.Array, valueRequired);
  252. CurrentSchema.Id = GetTypeId(type, false);
  253. JsonArrayAttribute arrayAttribute = JsonTypeReflector.GetCachedAttribute<JsonArrayAttribute>(type);
  254. bool allowNullItem = (arrayAttribute == null || arrayAttribute.AllowNullItems);
  255. Type collectionItemType = ReflectionUtils.GetCollectionItemType(type);
  256. if (collectionItemType != null)
  257. {
  258. CurrentSchema.Items = new List<JsonSchema>();
  259. CurrentSchema.Items.Add(GenerateInternal(collectionItemType, (!allowNullItem) ? Required.Always : Required.Default, false));
  260. }
  261. break;
  262. case JsonContractType.Primitive:
  263. CurrentSchema.Type = GetJsonSchemaType(type, valueRequired);
  264. if (CurrentSchema.Type == JsonSchemaType.Integer && type.IsEnum() && !type.IsDefined(typeof(FlagsAttribute), true))
  265. {
  266. CurrentSchema.Enum = new List<JToken>();
  267. EnumInfo enumValues = EnumUtils.GetEnumValuesAndNames(type);
  268. for (int i = 0; i < enumValues.Names.Length; i++)
  269. {
  270. ulong v = enumValues.Values[i];
  271. JToken value = JToken.FromObject(Enum.ToObject(type, v));
  272. CurrentSchema.Enum.Add(value);
  273. }
  274. }
  275. break;
  276. case JsonContractType.String:
  277. JsonSchemaType schemaType = (!ReflectionUtils.IsNullable(contract.UnderlyingType))
  278. ? JsonSchemaType.String
  279. : AddNullType(JsonSchemaType.String, valueRequired);
  280. CurrentSchema.Type = schemaType;
  281. break;
  282. case JsonContractType.Dictionary:
  283. CurrentSchema.Type = AddNullType(JsonSchemaType.Object, valueRequired);
  284. Type keyType;
  285. Type valueType;
  286. ReflectionUtils.GetDictionaryKeyValueTypes(type, out keyType, out valueType);
  287. if (keyType != null)
  288. {
  289. JsonContract keyContract = ContractResolver.ResolveContract(keyType);
  290. // can be converted to a string
  291. if (keyContract.ContractType == JsonContractType.Primitive)
  292. {
  293. CurrentSchema.AdditionalProperties = GenerateInternal(valueType, Required.Default, false);
  294. }
  295. }
  296. break;
  297. #if HAVE_BINARY_SERIALIZATION
  298. case JsonContractType.Serializable:
  299. CurrentSchema.Type = AddNullType(JsonSchemaType.Object, valueRequired);
  300. CurrentSchema.Id = GetTypeId(type, false);
  301. GenerateISerializableContract(type, (JsonISerializableContract)contract);
  302. break;
  303. #endif
  304. #if !NET20
  305. case JsonContractType.Dynamic:
  306. #endif
  307. case JsonContractType.Linq:
  308. CurrentSchema.Type = JsonSchemaType.Any;
  309. break;
  310. default:
  311. throw new JsonException("Unexpected contract type: {0}".FormatWith(CultureInfo.InvariantCulture, contract));
  312. }
  313. }
  314. return Pop().Schema;
  315. }
  316. private JsonSchemaType AddNullType(JsonSchemaType type, Required valueRequired)
  317. {
  318. if (valueRequired != Required.Always)
  319. {
  320. return type | JsonSchemaType.Null;
  321. }
  322. return type;
  323. }
  324. private bool HasFlag(DefaultValueHandling value, DefaultValueHandling flag)
  325. {
  326. return ((value & flag) == flag);
  327. }
  328. private void GenerateObjectSchema(Type type, JsonObjectContract contract)
  329. {
  330. CurrentSchema.Properties = new Dictionary<string, JsonSchema>();
  331. foreach (JsonProperty property in contract.Properties)
  332. {
  333. if (!property.Ignored)
  334. {
  335. bool optional = property.NullValueHandling == NullValueHandling.Ignore ||
  336. HasFlag(property.DefaultValueHandling.GetValueOrDefault(), DefaultValueHandling.Ignore) ||
  337. property.ShouldSerialize != null ||
  338. property.GetIsSpecified != null;
  339. JsonSchema propertySchema = GenerateInternal(property.PropertyType, property.Required, !optional);
  340. if (property.DefaultValue != null)
  341. {
  342. propertySchema.Default = JToken.FromObject(property.DefaultValue);
  343. }
  344. CurrentSchema.Properties.Add(property.PropertyName, propertySchema);
  345. }
  346. }
  347. if (type.IsSealed())
  348. {
  349. CurrentSchema.AllowAdditionalProperties = false;
  350. }
  351. }
  352. #if HAVE_BINARY_SERIALIZATION
  353. private void GenerateISerializableContract(Type type, JsonISerializableContract contract)
  354. {
  355. CurrentSchema.AllowAdditionalProperties = true;
  356. }
  357. #endif
  358. internal static bool HasFlag(JsonSchemaType? value, JsonSchemaType flag)
  359. {
  360. // default value is Any
  361. if (value == null)
  362. {
  363. return true;
  364. }
  365. bool match = ((value & flag) == flag);
  366. if (match)
  367. {
  368. return true;
  369. }
  370. // integer is a subset of float
  371. if (flag == JsonSchemaType.Integer && (value & JsonSchemaType.Float) == JsonSchemaType.Float)
  372. {
  373. return true;
  374. }
  375. return false;
  376. }
  377. private JsonSchemaType GetJsonSchemaType(Type type, Required valueRequired)
  378. {
  379. JsonSchemaType schemaType = JsonSchemaType.None;
  380. if (valueRequired != Required.Always && ReflectionUtils.IsNullable(type))
  381. {
  382. schemaType = JsonSchemaType.Null;
  383. if (ReflectionUtils.IsNullableType(type))
  384. {
  385. type = Nullable.GetUnderlyingType(type);
  386. }
  387. }
  388. PrimitiveTypeCode typeCode = ConvertUtils.GetTypeCode(type);
  389. switch (typeCode)
  390. {
  391. case PrimitiveTypeCode.Empty:
  392. case PrimitiveTypeCode.Object:
  393. return schemaType | JsonSchemaType.String;
  394. case PrimitiveTypeCode.DBNull:
  395. return schemaType | JsonSchemaType.Null;
  396. case PrimitiveTypeCode.Boolean:
  397. return schemaType | JsonSchemaType.Boolean;
  398. case PrimitiveTypeCode.Char:
  399. return schemaType | JsonSchemaType.String;
  400. case PrimitiveTypeCode.SByte:
  401. case PrimitiveTypeCode.Byte:
  402. case PrimitiveTypeCode.Int16:
  403. case PrimitiveTypeCode.UInt16:
  404. case PrimitiveTypeCode.Int32:
  405. case PrimitiveTypeCode.UInt32:
  406. case PrimitiveTypeCode.Int64:
  407. case PrimitiveTypeCode.UInt64:
  408. #if HAVE_BIG_INTEGER
  409. case PrimitiveTypeCode.BigInteger:
  410. #endif
  411. return schemaType | JsonSchemaType.Integer;
  412. case PrimitiveTypeCode.Single:
  413. case PrimitiveTypeCode.Double:
  414. case PrimitiveTypeCode.Decimal:
  415. return schemaType | JsonSchemaType.Float;
  416. // convert to string?
  417. case PrimitiveTypeCode.DateTime:
  418. #if !NET20
  419. case PrimitiveTypeCode.DateTimeOffset:
  420. #endif
  421. return schemaType | JsonSchemaType.String;
  422. case PrimitiveTypeCode.String:
  423. case PrimitiveTypeCode.Uri:
  424. case PrimitiveTypeCode.Guid:
  425. case PrimitiveTypeCode.TimeSpan:
  426. case PrimitiveTypeCode.Bytes:
  427. return schemaType | JsonSchemaType.String;
  428. default:
  429. throw new JsonException("Unexpected type code '{0}' for type '{1}'.".FormatWith(CultureInfo.InvariantCulture, typeCode, type));
  430. }
  431. }
  432. }
  433. }