@signe/schema-to-zod 2.8.3 → 2.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from 'vitest'
2
- import { jsonSchemaToZod } from '../src'
2
+ import { jsonSchemaToZod, jsonSchemaToZodSchema } from '../src'
3
3
  import { z } from 'zod'
4
4
 
5
5
  describe('jsonSchemaToZod', () => {
@@ -981,4 +981,588 @@ describe('jsonSchemaToZod', () => {
981
981
  }).success).toBeFalsy()
982
982
  })
983
983
  })
984
- })
984
+
985
+ describe('Should handle conditional object branches correctly', () => {
986
+ const itemSchema = {
987
+ type: 'object' as const,
988
+ properties: {
989
+ name: {
990
+ type: 'string' as const,
991
+ },
992
+ itemType: {
993
+ type: 'string' as const,
994
+ enum: ['item', 'weapon', 'armor'],
995
+ },
996
+ price: {
997
+ type: 'number' as const,
998
+ },
999
+ },
1000
+ required: ['name', 'itemType'],
1001
+ allOf: [
1002
+ {
1003
+ if: {
1004
+ properties: {
1005
+ itemType: { const: 'weapon' },
1006
+ },
1007
+ },
1008
+ then: {
1009
+ properties: {
1010
+ atk: {
1011
+ type: 'number' as const,
1012
+ },
1013
+ element: {
1014
+ type: 'string' as const,
1015
+ enum: ['none', 'fire', 'water'],
1016
+ },
1017
+ weaponType: {
1018
+ type: 'string' as const,
1019
+ enum: ['sword', 'axe', 'bow'],
1020
+ },
1021
+ },
1022
+ required: ['atk', 'element', 'weaponType'],
1023
+ },
1024
+ else: {
1025
+ if: {
1026
+ properties: {
1027
+ itemType: { const: 'armor' },
1028
+ },
1029
+ },
1030
+ then: {
1031
+ properties: {
1032
+ pdef: {
1033
+ type: 'number' as const,
1034
+ },
1035
+ armorType: {
1036
+ type: 'string' as const,
1037
+ enum: ['helmet', 'chest', 'shield'],
1038
+ },
1039
+ },
1040
+ required: ['pdef', 'armorType'],
1041
+ },
1042
+ },
1043
+ },
1044
+ ],
1045
+ }
1046
+
1047
+ test('Should validate only the weapon branch when itemType is weapon', async () => {
1048
+ const zodSchema = jsonSchemaToZodSchema(itemSchema as any)
1049
+
1050
+ expect(zodSchema.safeParse({
1051
+ name: 'Sword',
1052
+ itemType: 'weapon',
1053
+ price: 10,
1054
+ atk: 12,
1055
+ element: 'fire',
1056
+ weaponType: 'sword',
1057
+ pdef: 'ignored',
1058
+ }).success).toBeTruthy()
1059
+
1060
+ expect(zodSchema.safeParse({
1061
+ name: 'Sword',
1062
+ itemType: 'weapon',
1063
+ price: 10,
1064
+ element: 'fire',
1065
+ weaponType: 'sword',
1066
+ }).success).toBeFalsy()
1067
+
1068
+ expect(zodSchema.safeParse({
1069
+ name: 'Sword',
1070
+ itemType: 'weapon',
1071
+ price: 10,
1072
+ atk: 'high',
1073
+ element: 'fire',
1074
+ weaponType: 'sword',
1075
+ }).success).toBeFalsy()
1076
+ })
1077
+
1078
+ test('Should validate only the armor branch when itemType is armor', async () => {
1079
+ const zodSchema = jsonSchemaToZodSchema(itemSchema as any)
1080
+
1081
+ expect(zodSchema.safeParse({
1082
+ name: 'Shield',
1083
+ itemType: 'armor',
1084
+ price: 20,
1085
+ pdef: 8,
1086
+ armorType: 'shield',
1087
+ atk: 'ignored',
1088
+ }).success).toBeTruthy()
1089
+
1090
+ expect(zodSchema.safeParse({
1091
+ name: 'Shield',
1092
+ itemType: 'armor',
1093
+ price: 20,
1094
+ armorType: 'shield',
1095
+ }).success).toBeFalsy()
1096
+
1097
+ expect(zodSchema.safeParse({
1098
+ name: 'Shield',
1099
+ itemType: 'armor',
1100
+ price: 20,
1101
+ pdef: 'high',
1102
+ armorType: 'shield',
1103
+ }).success).toBeFalsy()
1104
+ })
1105
+
1106
+ test('Should ignore conditional branches when itemType is item', async () => {
1107
+ const zodSchema = jsonSchemaToZodSchema(itemSchema as any)
1108
+
1109
+ expect(zodSchema.safeParse({
1110
+ name: 'Potion',
1111
+ itemType: 'item',
1112
+ price: 5,
1113
+ atk: 'ignored',
1114
+ pdef: 'ignored',
1115
+ }).success).toBeTruthy()
1116
+ })
1117
+ })
1118
+
1119
+ describe('Should handle dependentRequired correctly', () => {
1120
+ test('Should enforce one-way dependencies only when the trigger property is present', async () => {
1121
+ const schema = {
1122
+ type: 'object' as const,
1123
+ properties: {
1124
+ name: { type: 'string' as const },
1125
+ credit_card: { type: 'number' as const },
1126
+ billing_address: { type: 'string' as const },
1127
+ },
1128
+ required: ['name'],
1129
+ dependentRequired: {
1130
+ credit_card: ['billing_address'],
1131
+ },
1132
+ }
1133
+
1134
+ const zodSchema = jsonSchemaToZodSchema(schema as any)
1135
+
1136
+ expect(zodSchema.safeParse({
1137
+ name: 'John Doe',
1138
+ credit_card: 5555555555555555,
1139
+ billing_address: '555 Debtor Lane',
1140
+ }).success).toBeTruthy()
1141
+
1142
+ expect(zodSchema.safeParse({
1143
+ name: 'John Doe',
1144
+ credit_card: 5555555555555555,
1145
+ }).success).toBeFalsy()
1146
+
1147
+ expect(zodSchema.safeParse({
1148
+ name: 'John Doe',
1149
+ }).success).toBeTruthy()
1150
+
1151
+ expect(zodSchema.safeParse({
1152
+ name: 'John Doe',
1153
+ billing_address: '555 Debtor Lane',
1154
+ }).success).toBeTruthy()
1155
+ })
1156
+
1157
+ test('Should support bidirectional dependencies when both sides are declared', async () => {
1158
+ const schema = {
1159
+ type: 'object' as const,
1160
+ properties: {
1161
+ name: { type: 'string' as const },
1162
+ credit_card: { type: 'number' as const },
1163
+ billing_address: { type: 'string' as const },
1164
+ },
1165
+ required: ['name'],
1166
+ dependentRequired: {
1167
+ credit_card: ['billing_address'],
1168
+ billing_address: ['credit_card'],
1169
+ },
1170
+ }
1171
+
1172
+ const zodSchema = jsonSchemaToZodSchema(schema as any)
1173
+
1174
+ expect(zodSchema.safeParse({
1175
+ name: 'John Doe',
1176
+ credit_card: 5555555555555555,
1177
+ billing_address: '555 Debtor Lane',
1178
+ }).success).toBeTruthy()
1179
+
1180
+ expect(zodSchema.safeParse({
1181
+ name: 'John Doe',
1182
+ credit_card: 5555555555555555,
1183
+ }).success).toBeFalsy()
1184
+
1185
+ expect(zodSchema.safeParse({
1186
+ name: 'John Doe',
1187
+ billing_address: '555 Debtor Lane',
1188
+ }).success).toBeFalsy()
1189
+
1190
+ expect(zodSchema.safeParse({
1191
+ name: 'John Doe',
1192
+ }).success).toBeTruthy()
1193
+ })
1194
+ })
1195
+
1196
+ describe('Should handle dependentSchemas correctly', () => {
1197
+ test('Should apply the dependent schema independently when the trigger property is present', async () => {
1198
+ const schema = {
1199
+ type: 'object' as const,
1200
+ properties: {
1201
+ name: { type: 'string' as const },
1202
+ credit_card: { type: 'number' as const },
1203
+ },
1204
+ required: ['name'],
1205
+ dependentSchemas: {
1206
+ credit_card: {
1207
+ properties: {
1208
+ billing_address: { type: 'string' as const },
1209
+ },
1210
+ required: ['billing_address'],
1211
+ },
1212
+ },
1213
+ }
1214
+
1215
+ const zodSchema = jsonSchemaToZodSchema(schema as any)
1216
+
1217
+ expect(zodSchema.safeParse({
1218
+ name: 'John Doe',
1219
+ credit_card: 5555555555555555,
1220
+ billing_address: '555 Debtor Lane',
1221
+ }).success).toBeTruthy()
1222
+
1223
+ expect(zodSchema.safeParse({
1224
+ name: 'John Doe',
1225
+ credit_card: 5555555555555555,
1226
+ }).success).toBeFalsy()
1227
+
1228
+ expect(zodSchema.safeParse({
1229
+ name: 'John Doe',
1230
+ billing_address: '555 Debtor Lane',
1231
+ }).success).toBeTruthy()
1232
+ })
1233
+ })
1234
+
1235
+ describe('Should handle if/then/else semantics correctly', () => {
1236
+ test('Should follow the truth table when both then and else are defined', async () => {
1237
+ const schema = {
1238
+ type: 'object' as const,
1239
+ properties: {
1240
+ mode: {
1241
+ type: 'string' as const,
1242
+ enum: ['strict', 'loose'],
1243
+ },
1244
+ strictValue: { type: 'number' as const },
1245
+ looseValue: { type: 'boolean' as const },
1246
+ },
1247
+ if: {
1248
+ properties: {
1249
+ mode: { const: 'strict' },
1250
+ },
1251
+ required: ['mode'],
1252
+ },
1253
+ then: {
1254
+ required: ['strictValue'],
1255
+ },
1256
+ else: {
1257
+ required: ['looseValue'],
1258
+ },
1259
+ }
1260
+
1261
+ const zodSchema = jsonSchemaToZodSchema(schema as any)
1262
+
1263
+ expect(zodSchema.safeParse({
1264
+ mode: 'strict',
1265
+ strictValue: 10,
1266
+ }).success).toBeTruthy()
1267
+
1268
+ expect(zodSchema.safeParse({
1269
+ mode: 'strict',
1270
+ }).success).toBeFalsy()
1271
+
1272
+ expect(zodSchema.safeParse({
1273
+ mode: 'loose',
1274
+ looseValue: true,
1275
+ }).success).toBeTruthy()
1276
+
1277
+ expect(zodSchema.safeParse({
1278
+ mode: 'loose',
1279
+ }).success).toBeFalsy()
1280
+ })
1281
+
1282
+ test('Should ignore then and else when if is not defined', async () => {
1283
+ const schema = {
1284
+ type: 'object' as const,
1285
+ properties: {
1286
+ mode: { type: 'string' as const },
1287
+ },
1288
+ then: {
1289
+ required: ['strictValue'],
1290
+ },
1291
+ else: {
1292
+ required: ['looseValue'],
1293
+ },
1294
+ }
1295
+
1296
+ const zodSchema = jsonSchemaToZodSchema(schema as any)
1297
+
1298
+ expect(zodSchema.safeParse({
1299
+ mode: 'anything',
1300
+ }).success).toBeTruthy()
1301
+ })
1302
+
1303
+ test('Should treat an if without then or else as having no effect', async () => {
1304
+ const schema = {
1305
+ type: 'object' as const,
1306
+ properties: {
1307
+ country: { type: 'string' as const },
1308
+ },
1309
+ if: {
1310
+ properties: {
1311
+ country: { const: 'Canada' },
1312
+ },
1313
+ },
1314
+ }
1315
+
1316
+ const zodSchema = jsonSchemaToZodSchema(schema as any)
1317
+
1318
+ expect(zodSchema.safeParse({ country: 'Canada' }).success).toBeTruthy()
1319
+ expect(zodSchema.safeParse({ country: 'France' }).success).toBeTruthy()
1320
+ expect(zodSchema.safeParse({}).success).toBeTruthy()
1321
+ })
1322
+ })
1323
+
1324
+ describe('Should handle country-based postal code conditionals from the JSON Schema docs', () => {
1325
+ test('Should default to USA validation when country is not required in the if branch', async () => {
1326
+ const schema = {
1327
+ type: 'object' as const,
1328
+ properties: {
1329
+ street_address: { type: 'string' as const },
1330
+ country: {
1331
+ type: 'string' as const,
1332
+ enum: ['United States of America', 'Canada'],
1333
+ },
1334
+ postal_code: { type: 'string' as const },
1335
+ },
1336
+ if: {
1337
+ properties: {
1338
+ country: { const: 'United States of America' },
1339
+ },
1340
+ },
1341
+ then: {
1342
+ properties: {
1343
+ postal_code: { pattern: '[0-9]{5}(-[0-9]{4})?' },
1344
+ },
1345
+ },
1346
+ else: {
1347
+ properties: {
1348
+ postal_code: { pattern: '[A-Z][0-9][A-Z] [0-9][A-Z][0-9]' },
1349
+ },
1350
+ },
1351
+ }
1352
+
1353
+ const zodSchema = jsonSchemaToZodSchema(schema as any)
1354
+
1355
+ expect(zodSchema.safeParse({
1356
+ street_address: '1600 Pennsylvania Avenue NW',
1357
+ country: 'United States of America',
1358
+ postal_code: '20500',
1359
+ }).success).toBeTruthy()
1360
+
1361
+ expect(zodSchema.safeParse({
1362
+ street_address: '1600 Pennsylvania Avenue NW',
1363
+ postal_code: '20500',
1364
+ }).success).toBeTruthy()
1365
+
1366
+ expect(zodSchema.safeParse({
1367
+ street_address: '24 Sussex Drive',
1368
+ country: 'Canada',
1369
+ postal_code: 'K1M 1M4',
1370
+ }).success).toBeTruthy()
1371
+
1372
+ expect(zodSchema.safeParse({
1373
+ street_address: '24 Sussex Drive',
1374
+ country: 'Canada',
1375
+ postal_code: '10000',
1376
+ }).success).toBeFalsy()
1377
+
1378
+ expect(zodSchema.safeParse({
1379
+ street_address: '1600 Pennsylvania Avenue NW',
1380
+ postal_code: 'K1M 1M4',
1381
+ }).success).toBeFalsy()
1382
+ })
1383
+
1384
+ test('Should support scalable allOf conditionals for multiple countries', async () => {
1385
+ const schema = {
1386
+ type: 'object' as const,
1387
+ properties: {
1388
+ street_address: { type: 'string' as const },
1389
+ country: {
1390
+ type: 'string' as const,
1391
+ enum: ['United States of America', 'Canada', 'Netherlands'],
1392
+ },
1393
+ postal_code: { type: 'string' as const },
1394
+ },
1395
+ allOf: [
1396
+ {
1397
+ if: {
1398
+ properties: {
1399
+ country: { const: 'United States of America' },
1400
+ },
1401
+ },
1402
+ then: {
1403
+ properties: {
1404
+ postal_code: { pattern: '[0-9]{5}(-[0-9]{4})?' },
1405
+ },
1406
+ },
1407
+ },
1408
+ {
1409
+ if: {
1410
+ properties: {
1411
+ country: { const: 'Canada' },
1412
+ },
1413
+ required: ['country'],
1414
+ },
1415
+ then: {
1416
+ properties: {
1417
+ postal_code: { pattern: '[A-Z][0-9][A-Z] [0-9][A-Z][0-9]' },
1418
+ },
1419
+ },
1420
+ },
1421
+ {
1422
+ if: {
1423
+ properties: {
1424
+ country: { const: 'Netherlands' },
1425
+ },
1426
+ required: ['country'],
1427
+ },
1428
+ then: {
1429
+ properties: {
1430
+ postal_code: { pattern: '[0-9]{4} [A-Z]{2}' },
1431
+ },
1432
+ },
1433
+ },
1434
+ ],
1435
+ }
1436
+
1437
+ const zodSchema = jsonSchemaToZodSchema(schema as any)
1438
+
1439
+ expect(zodSchema.safeParse({
1440
+ street_address: '1600 Pennsylvania Avenue NW',
1441
+ country: 'United States of America',
1442
+ postal_code: '20500',
1443
+ }).success).toBeTruthy()
1444
+
1445
+ expect(zodSchema.safeParse({
1446
+ street_address: '1600 Pennsylvania Avenue NW',
1447
+ postal_code: '20500',
1448
+ }).success).toBeTruthy()
1449
+
1450
+ expect(zodSchema.safeParse({
1451
+ street_address: '24 Sussex Drive',
1452
+ country: 'Canada',
1453
+ postal_code: 'K1M 1M4',
1454
+ }).success).toBeTruthy()
1455
+
1456
+ expect(zodSchema.safeParse({
1457
+ street_address: 'Adriaan Goekooplaan',
1458
+ country: 'Netherlands',
1459
+ postal_code: '2517 JX',
1460
+ }).success).toBeTruthy()
1461
+
1462
+ expect(zodSchema.safeParse({
1463
+ street_address: '24 Sussex Drive',
1464
+ country: 'Canada',
1465
+ postal_code: '10000',
1466
+ }).success).toBeFalsy()
1467
+
1468
+ expect(zodSchema.safeParse({
1469
+ street_address: '1600 Pennsylvania Avenue NW',
1470
+ postal_code: 'K1M 1M4',
1471
+ }).success).toBeFalsy()
1472
+ })
1473
+ })
1474
+
1475
+ describe('Should handle anyOf correctly', () => {
1476
+ test('Should validate a property when at least one anyOf branch matches', async () => {
1477
+ const schema = {
1478
+ type: 'object' as const,
1479
+ properties: {
1480
+ value: {
1481
+ anyOf: [
1482
+ { type: 'string' as const, minLength: 3 },
1483
+ { type: 'number' as const, minimum: 10 },
1484
+ ],
1485
+ },
1486
+ },
1487
+ }
1488
+
1489
+ const zodSchema = jsonSchemaToZodSchema(schema as any)
1490
+
1491
+ expect(zodSchema.safeParse({ value: 'abcd' }).success).toBeTruthy()
1492
+ expect(zodSchema.safeParse({ value: 12 }).success).toBeTruthy()
1493
+ expect(zodSchema.safeParse({ value: 'ab' }).success).toBeFalsy()
1494
+ expect(zodSchema.safeParse({ value: 5 }).success).toBeFalsy()
1495
+ expect(zodSchema.safeParse({ value: false }).success).toBeFalsy()
1496
+ })
1497
+ })
1498
+
1499
+ describe('Should handle not correctly', () => {
1500
+ test('Should reject values that match the not schema', async () => {
1501
+ const schema = {
1502
+ type: 'object' as const,
1503
+ properties: {
1504
+ value: {
1505
+ not: { type: 'string' as const },
1506
+ },
1507
+ },
1508
+ }
1509
+
1510
+ const zodSchema = jsonSchemaToZodSchema(schema as any)
1511
+
1512
+ expect(zodSchema.safeParse({ value: 42 }).success).toBeTruthy()
1513
+ expect(zodSchema.safeParse({ value: true }).success).toBeTruthy()
1514
+ expect(zodSchema.safeParse({ value: 'forbidden' }).success).toBeFalsy()
1515
+ })
1516
+ })
1517
+
1518
+ describe('Should handle implication patterns with anyOf and not', () => {
1519
+ test('Should support the sit-down restaurant implies tip example from the JSON Schema docs', async () => {
1520
+ const schema = {
1521
+ type: 'object' as const,
1522
+ properties: {
1523
+ restaurantType: {
1524
+ type: 'string' as const,
1525
+ enum: ['fast-food', 'sit-down'],
1526
+ },
1527
+ total: { type: 'number' as const },
1528
+ tip: { type: 'number' as const },
1529
+ },
1530
+ anyOf: [
1531
+ {
1532
+ not: {
1533
+ properties: {
1534
+ restaurantType: { const: 'sit-down' },
1535
+ },
1536
+ required: ['restaurantType'],
1537
+ },
1538
+ },
1539
+ {
1540
+ required: ['tip'],
1541
+ },
1542
+ ],
1543
+ }
1544
+
1545
+ const zodSchema = jsonSchemaToZodSchema(schema as any)
1546
+
1547
+ expect(zodSchema.safeParse({
1548
+ restaurantType: 'sit-down',
1549
+ total: 16.99,
1550
+ tip: 3.4,
1551
+ }).success).toBeTruthy()
1552
+
1553
+ expect(zodSchema.safeParse({
1554
+ restaurantType: 'sit-down',
1555
+ total: 16.99,
1556
+ }).success).toBeFalsy()
1557
+
1558
+ expect(zodSchema.safeParse({
1559
+ restaurantType: 'fast-food',
1560
+ total: 6.99,
1561
+ }).success).toBeTruthy()
1562
+
1563
+ expect(zodSchema.safeParse({
1564
+ total: 5.25,
1565
+ }).success).toBeTruthy()
1566
+ })
1567
+ })
1568
+ })