coercion.spec.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508
  1. 'use strict';
  2. var Ajv = require('./ajv');
  3. require('./chai').should();
  4. var coercionRules = {
  5. 'string': {
  6. 'number': [
  7. { from: 1, to: '1' },
  8. { from: 1.5, to: '1.5' },
  9. { from: 2e100, to: '2e+100' }
  10. ],
  11. 'boolean': [
  12. { from: false, to: 'false' },
  13. { from: true, to: 'true' }
  14. ],
  15. 'null': [
  16. { from: null, to: '' }
  17. ],
  18. 'object': [
  19. { from: {}, to: undefined }
  20. ],
  21. 'array': [
  22. { from: [], to: undefined },
  23. { from: [1], to: undefined }
  24. ]
  25. },
  26. 'number': {
  27. 'string': [
  28. { from: '1', to: 1 },
  29. { from: '1.5', to: 1.5 },
  30. { from: '2e10', to: 2e10 },
  31. { from: '1a', to: undefined },
  32. { from: 'abc', to: undefined },
  33. { from: '', to: undefined }
  34. ],
  35. 'boolean': [
  36. { from: false, to: 0 },
  37. { from: true, to: 1 }
  38. ],
  39. 'null': [
  40. { from: null, to: 0 }
  41. ],
  42. 'object': [
  43. { from: {}, to: undefined }
  44. ],
  45. 'array': [
  46. { from: [], to: undefined },
  47. { from: [true], to: undefined }
  48. ]
  49. },
  50. 'integer': {
  51. 'string': [
  52. { from: '1', to: 1 },
  53. { from: '1.5', to: undefined },
  54. { from: '2e10', to: 2e10 },
  55. { from: '1a', to: undefined },
  56. { from: 'abc', to: undefined },
  57. { from: '', to: undefined }
  58. ],
  59. 'boolean': [
  60. { from: false, to: 0 },
  61. { from: true, to: 1 }
  62. ],
  63. 'null': [
  64. { from: null, to: 0 }
  65. ],
  66. 'object': [
  67. { from: {}, to: undefined }
  68. ],
  69. 'array': [
  70. { from: [], to: undefined },
  71. { from: ['1'], to: undefined }
  72. ]
  73. },
  74. 'boolean': {
  75. 'string': [
  76. { from: 'false', to: false },
  77. { from: 'true', to: true },
  78. { from: '', to: undefined },
  79. { from: 'abc', to: undefined }
  80. ],
  81. 'number': [
  82. { from: 0, to: false },
  83. { from: 1, to: true },
  84. { from: 2, to: undefined },
  85. { from: 2.5, to: undefined }
  86. ],
  87. 'null': [
  88. { from: null, to: false }
  89. ],
  90. 'object': [
  91. { from: {}, to: undefined }
  92. ],
  93. 'array': [
  94. { from: [], to: undefined },
  95. { from: [0], to: undefined }
  96. ]
  97. },
  98. 'null': {
  99. 'string': [
  100. { from: '', to: null },
  101. { from: 'abc', to: undefined },
  102. { from: 'null', to: undefined }
  103. ],
  104. 'number': [
  105. { from: 0, to: null },
  106. { from: 1, to: undefined }
  107. ],
  108. 'boolean': [
  109. { from: false, to: null },
  110. { from: true, to: undefined }
  111. ],
  112. 'object': [
  113. { from: {}, to: undefined }
  114. ],
  115. 'array': [
  116. { from: [], to: undefined },
  117. { from: [null], to: undefined }
  118. ]
  119. },
  120. 'array': {
  121. 'all': [
  122. { type: 'string', from: 'abc', to: undefined },
  123. { type: 'number', from: 1, to: undefined },
  124. { type: 'boolean', from: true, to: undefined },
  125. { type: 'null', from: null, to: undefined },
  126. { type: 'object', from: {}, to: undefined }
  127. ]
  128. },
  129. 'object': {
  130. 'all': [
  131. { type: 'string', from: 'abc', to: undefined },
  132. { type: 'number', from: 1, to: undefined },
  133. { type: 'boolean', from: true, to: undefined },
  134. { type: 'null', from: null, to: undefined },
  135. { type: 'array', from: [], to: undefined }
  136. ]
  137. }
  138. };
  139. var coercionArrayRules = JSON.parse(JSON.stringify(coercionRules));
  140. coercionArrayRules.string.array = [
  141. { from: ['abc'], to: 'abc' },
  142. { from: [123], to: '123' },
  143. { from: ['abc', 'def'], to: undefined },
  144. { from: [], to: undefined }
  145. ];
  146. coercionArrayRules.number.array = [
  147. { from: [1.5], to: 1.5 },
  148. { from: ['1.5'], to: 1.5 }
  149. ];
  150. coercionArrayRules.integer.array = [
  151. { from: [1], to: 1 },
  152. { from: ['1'], to: 1 },
  153. { from: [true], to: 1 },
  154. { from: [null], to: 0 }
  155. ];
  156. coercionArrayRules.boolean.array = [
  157. { from: [true], to: true },
  158. { from: ['true'], to: true },
  159. { from: [1], to: true }
  160. ];
  161. coercionArrayRules.null.array = [
  162. { from: [null], to: null },
  163. { from: [''], to: null },
  164. { from: [0], to: null },
  165. { from: [false], to: null }
  166. ];
  167. coercionArrayRules.object.array = [
  168. { from: [{}], to: undefined }
  169. ];
  170. coercionArrayRules.array = {
  171. 'string': [
  172. {from: 'abc', to: ['abc']}
  173. ],
  174. 'number': [
  175. {from: 1, to: [1]}
  176. ],
  177. 'boolean': [
  178. {from: true, to: [true]}
  179. ],
  180. 'null': [
  181. {from: null, to: [null]}
  182. ],
  183. 'object': [
  184. {from: {}, to: undefined}
  185. ]
  186. };
  187. describe('Type coercion', function () {
  188. var ajv, fullAjv, instances;
  189. beforeEach(function() {
  190. ajv = new Ajv({ coerceTypes: true, verbose: true });
  191. fullAjv = new Ajv({ coerceTypes: true, verbose: true, allErrors: true });
  192. instances = [ ajv, fullAjv ];
  193. });
  194. it('should coerce scalar values', function() {
  195. testRules(coercionRules, function (test, schema, canCoerce/*, toType, fromType*/) {
  196. instances.forEach(function (_ajv) {
  197. var valid = _ajv.validate(schema, test.from);
  198. //if (valid !== canCoerce) console.log('true', toType, fromType, test, ajv.errors);
  199. valid. should.equal(canCoerce);
  200. });
  201. });
  202. });
  203. it('should coerce scalar values (coerceTypes = array)', function() {
  204. ajv = new Ajv({ coerceTypes: 'array', verbose: true });
  205. fullAjv = new Ajv({ coerceTypes: 'array', verbose: true, allErrors: true });
  206. instances = [ ajv, fullAjv ];
  207. testRules(coercionArrayRules, function (test, schema, canCoerce, toType, fromType) {
  208. instances.forEach(function (_ajv) {
  209. var valid = _ajv.validate(schema, test.from);
  210. if (valid !== canCoerce) console.log(toType, '.', fromType, test, schema, ajv.errors);
  211. valid. should.equal(canCoerce);
  212. });
  213. });
  214. });
  215. it('should coerce values in objects/arrays and update properties/items', function() {
  216. testRules(coercionRules, function (test, schema, canCoerce/*, toType, fromType*/) {
  217. var schemaObject = {
  218. type: 'object',
  219. properties: {
  220. foo: schema
  221. }
  222. };
  223. var schemaArray = {
  224. type: 'array',
  225. items: schema
  226. };
  227. var schemaArrObj = {
  228. type: 'array',
  229. items: schemaObject
  230. };
  231. instances.forEach(function (_ajv) {
  232. testCoercion(_ajv, schemaArray, [ test.from ], [ test.to ]);
  233. testCoercion(_ajv, schemaObject, { foo: test.from }, { foo: test.to });
  234. testCoercion(_ajv, schemaArrObj, [ { foo: test.from } ], [ { foo: test.to } ]);
  235. });
  236. function testCoercion(_ajv, _schema, fromData, toData) {
  237. var valid = _ajv.validate(_schema, fromData);
  238. //if (valid !== canCoerce) console.log(schema, fromData, toData);
  239. valid. should.equal(canCoerce);
  240. if (valid) fromData.should.eql(toData);
  241. }
  242. });
  243. });
  244. it('should coerce to multiple types in order with number type', function() {
  245. var schema = {
  246. type: 'object',
  247. properties: {
  248. foo: {
  249. type: [ 'number', 'boolean', 'null' ]
  250. }
  251. }
  252. };
  253. instances.forEach(function (_ajv) {
  254. var data;
  255. _ajv.validate(schema, data = { foo: '1' }) .should.equal(true);
  256. data .should.eql({ foo: 1 });
  257. _ajv.validate(schema, data = { foo: '1.5' }) .should.equal(true);
  258. data .should.eql({ foo: 1.5 });
  259. _ajv.validate(schema, data = { foo: 'false' }) .should.equal(true);
  260. data .should.eql({ foo: false });
  261. _ajv.validate(schema, data = { foo: 1 }) .should.equal(true);
  262. data .should.eql({ foo: 1 }); // no coercion
  263. _ajv.validate(schema, data = { foo: true }) .should.equal(true);
  264. data .should.eql({ foo: true }); // no coercion
  265. _ajv.validate(schema, data = { foo: null }) .should.equal(true);
  266. data .should.eql({ foo: null }); // no coercion
  267. _ajv.validate(schema, data = { foo: 'abc' }) .should.equal(false);
  268. data .should.eql({ foo: 'abc' }); // can't coerce
  269. _ajv.validate(schema, data = { foo: {} }) .should.equal(false);
  270. data .should.eql({ foo: {} }); // can't coerce
  271. _ajv.validate(schema, data = { foo: [] }) .should.equal(false);
  272. data .should.eql({ foo: [] }); // can't coerce
  273. });
  274. });
  275. it('should coerce to multiple types in order with integer type', function() {
  276. var schema = {
  277. type: 'object',
  278. properties: {
  279. foo: {
  280. type: [ 'integer', 'boolean', 'null' ]
  281. }
  282. }
  283. };
  284. instances.forEach(function (_ajv) {
  285. var data;
  286. _ajv.validate(schema, data = { foo: '1' }) .should.equal(true);
  287. data .should.eql({ foo: 1 });
  288. _ajv.validate(schema, data = { foo: 'false' }) .should.equal(true);
  289. data .should.eql({ foo: false });
  290. _ajv.validate(schema, data = { foo: 1 }) .should.equal(true);
  291. data .should.eql({ foo: 1 }); // no coercion
  292. _ajv.validate(schema, data = { foo: true }) .should.equal(true);
  293. data .should.eql({ foo: true }); // no coercion
  294. _ajv.validate(schema, data = { foo: null }) .should.equal(true);
  295. data .should.eql({ foo: null }); // no coercion
  296. _ajv.validate(schema, data = { foo: 'abc' }) .should.equal(false);
  297. data .should.eql({ foo: 'abc' }); // can't coerce
  298. _ajv.validate(schema, data = { foo: {} }) .should.equal(false);
  299. data .should.eql({ foo: {} }); // can't coerce
  300. _ajv.validate(schema, data = { foo: [] }) .should.equal(false);
  301. data .should.eql({ foo: [] }); // can't coerce
  302. });
  303. });
  304. it('should fail to coerce non-number if multiple properties/items are coerced (issue #152)', function() {
  305. var schema = {
  306. type: 'object',
  307. properties: {
  308. foo: { type: 'number' },
  309. bar: { type: 'number' }
  310. }
  311. };
  312. var schema2 = {
  313. type: 'array',
  314. items: { type: 'number' }
  315. };
  316. instances.forEach(function (_ajv) {
  317. var data = { foo: '123', bar: 'bar' };
  318. _ajv.validate(schema, data) .should.equal(false);
  319. data .should.eql({ foo: 123, bar: 'bar' });
  320. var data2 = [ '123', 'bar' ];
  321. _ajv.validate(schema2, data2) .should.equal(false);
  322. data2 .should.eql([ 123, 'bar' ]);
  323. });
  324. });
  325. it('should update data if the schema is in ref that is not inlined', function () {
  326. instances.push(new Ajv({ coerceTypes: true, inlineRefs: false }));
  327. var schema = {
  328. type: 'object',
  329. definitions: {
  330. foo: { type: 'number' }
  331. },
  332. properties: {
  333. foo: { $ref: '#/definitions/foo' }
  334. }
  335. };
  336. var schema2 = {
  337. type: 'object',
  338. definitions: {
  339. foo: {
  340. // allOf is needed to make sure that "foo" is compiled to a separate function
  341. // and not simply passed through (as it would be if it were only $ref)
  342. allOf: [{ $ref: '#/definitions/bar' }]
  343. },
  344. bar: { type: 'number' }
  345. },
  346. properties: {
  347. foo: { $ref: '#/definitions/foo' }
  348. }
  349. };
  350. var schemaRecursive = {
  351. type: [ 'object', 'number' ],
  352. properties: {
  353. foo: { $ref: '#' }
  354. }
  355. };
  356. var schemaRecursive2 = {
  357. $id: 'http://e.com/schema.json#',
  358. definitions: {
  359. foo: {
  360. $id: 'http://e.com/foo.json#',
  361. type: [ 'object', 'number' ],
  362. properties: {
  363. foo: { $ref: '#' }
  364. }
  365. }
  366. },
  367. properties: {
  368. foo: { $ref: 'http://e.com/foo.json#' }
  369. }
  370. };
  371. instances.forEach(function (_ajv) {
  372. testCoercion(schema, { foo: '1' }, { foo: 1 });
  373. testCoercion(schema2, { foo: '1' }, { foo: 1 });
  374. testCoercion(schemaRecursive, { foo: { foo: '1' } }, { foo: { foo: 1 } });
  375. testCoercion(schemaRecursive2, { foo: { foo: { foo: '1' } } },
  376. { foo: { foo: { foo: 1 } } });
  377. function testCoercion(_schema, fromData, toData) {
  378. var valid = _ajv.validate(_schema, fromData);
  379. // if (!valid) console.log(schema, fromData, toData);
  380. valid. should.equal(true);
  381. fromData .should.eql(toData);
  382. }
  383. });
  384. });
  385. it('should generate one error for type with coerceTypes option (issue #469)', function() {
  386. var schema = {
  387. "type": "number",
  388. "minimum": 10
  389. };
  390. instances.forEach(function (_ajv) {
  391. var validate = _ajv.compile(schema);
  392. validate(9). should.equal(false);
  393. validate.errors.length .should.equal(1);
  394. validate(11). should.equal(true);
  395. validate('foo'). should.equal(false);
  396. validate.errors.length .should.equal(1);
  397. });
  398. });
  399. it('should check "uniqueItems" after coercion', function() {
  400. var schema = {
  401. items: {type: 'number'},
  402. uniqueItems: true
  403. };
  404. instances.forEach(function (_ajv) {
  405. var validate = _ajv.compile(schema);
  406. validate([1, '2', 3]). should.equal(true);
  407. validate([1, '2', 2]). should.equal(false);
  408. validate.errors.length .should.equal(1);
  409. validate.errors[0].keyword .should.equal('uniqueItems');
  410. });
  411. });
  412. it('should check "contains" after coercion', function() {
  413. var schema = {
  414. items: {type: 'number'},
  415. contains: {const: 2}
  416. };
  417. instances.forEach(function (_ajv) {
  418. var validate = _ajv.compile(schema);
  419. validate([1, '2', 3]). should.equal(true);
  420. validate([1, '3', 4]). should.equal(false);
  421. validate.errors.pop().keyword .should.equal('contains');
  422. });
  423. });
  424. function testRules(rules, cb) {
  425. for (var toType in rules) {
  426. for (var fromType in rules[toType]) {
  427. var tests = rules[toType][fromType];
  428. tests.forEach(function (test) {
  429. var canCoerce = test.to !== undefined;
  430. var schema = canCoerce
  431. ? (Array.isArray(test.to)
  432. ? { "type": toType, "items": { "type": fromType, "enum": [ test.to[0] ] } }
  433. : { "type": toType, "enum": [ test.to ] })
  434. : { type: toType };
  435. cb(test, schema, canCoerce, toType, fromType);
  436. });
  437. }
  438. }
  439. }
  440. });