@makehq/forman-schema 1.16.0 → 1.17.0

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.
package/README.md CHANGED
@@ -291,11 +291,53 @@ const result = await validateFormanWithDomains(
291
291
  - object → collection
292
292
  - array → array
293
293
 
294
+ ## Field type resolution
295
+
296
+ Field types resolve through three steps, so schemas authored with loose casing or common synonyms
297
+ still convert:
298
+
299
+ 1. **Exact match** against `FORMAN_TYPE_MAP`.
300
+ 2. **Case-insensitive match** — `fileName`, `Boolean`, `URL`, `Select` resolve to their canonical
301
+ lowercase types. The index is derived from the map itself, so new entries get this for free.
302
+ 3. **Aliases** — `string→text`, `bool→boolean`, `datetime→date`, `float→number`,
303
+ `upload→filestorage`. Only unambiguous, information-preserving synonyms are aliased.
304
+
305
+ A `type:kind` suffix (`account:google`, `device:apn`) resolves on its base type and keeps the kind,
306
+ which drives the `api://` store expansion.
307
+
308
+ ### Unconvertible fields
309
+
310
+ A field whose type is **missing or unresolvable** is degraded to a permissive typeless schema (the
311
+ same shape `any` produces) instead of aborting the conversion, and its dot-notation path is reported
312
+ on `toJSONSchemaAdvanced`'s `skippedPaths.unconvertible`:
313
+
314
+ ```js
315
+ const { schema, skippedPaths } = toJSONSchemaAdvanced({
316
+ name: 'wrapper',
317
+ type: 'collection',
318
+ spec: [
319
+ { name: 'good', type: 'text' },
320
+ { name: 'odd', type: 'somethingNew' },
321
+ ],
322
+ });
323
+ // schema.properties → { good: { type: 'string' }, odd: {} }
324
+ // skippedPaths → { unconvertible: ['wrapper.odd (unknown type: somethingNew)'] }
325
+ ```
326
+
327
+ This is deliberate: the throw was fatal at any depth, so a single unrecognized leaf field destroyed
328
+ the whole schema and left consumers with nothing. Types requiring a guess about intent (`tags`,
329
+ `category`, `object`) are degraded rather than aliased — a degraded field is honest, a wrongly
330
+ aliased one is a lie the consumer will act on.
331
+
332
+ Pass `{ strictFieldTypes: true }` to restore fail-fast throwing.
333
+
294
334
  ## Error Handling
295
335
 
296
336
  ### SchemaConversionError
297
337
 
298
- `SchemaConversionError` is thrown when schema conversion fails. It includes a message and optionally the field that caused the error.
338
+ `SchemaConversionError` is thrown when schema conversion fails, and for unresolvable field types
339
+ only when `strictFieldTypes: true` is set. It carries a message and the `field` that caused the
340
+ error.
299
341
 
300
342
  ## Testing
301
343
 
package/dist/index.cjs CHANGED
@@ -20,6 +20,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ SchemaConversionError: () => SchemaConversionError,
24
+ resolveFormanFieldType: () => resolveFormanFieldType,
23
25
  toFormanSchema: () => toFormanSchema,
24
26
  toJSONSchema: () => toJSONSchema,
25
27
  toJSONSchemaAdvanced: () => toJSONSchemaAdvanced,
@@ -543,11 +545,13 @@ var SchemaConversionError = class extends Error {
543
545
  constructor(message, field) {
544
546
  super(message);
545
547
  this.name = "SchemaConversionError";
548
+ this.field = field;
546
549
  }
547
550
  };
548
551
  var FORMAN_TYPE_MAP = {
549
552
  account: "number",
550
553
  hook: "number",
554
+ device: "number",
551
555
  keychain: "number",
552
556
  datastore: "number",
553
557
  aiagent: "string",
@@ -591,14 +595,29 @@ var FORMAN_TYPE_MAP = {
591
595
  uuid: "string",
592
596
  any: void 0
593
597
  };
594
- function validateFormanField(field) {
595
- if (!field.type) {
596
- throw new SchemaConversionError("Field type is required", field);
597
- }
598
- const normalizedType = field.type.includes(":") ? field.type.split(":")[0] : field.type;
599
- if (!Object.keys(FORMAN_TYPE_MAP).includes(normalizedType)) {
600
- throw new SchemaConversionError(`Unknown field type: ${field.type}`, field);
601
- }
598
+ var FORMAN_TYPE_LOOKUP = new Map(
599
+ Object.keys(FORMAN_TYPE_MAP).map((key) => [key.toLowerCase(), key])
600
+ );
601
+ var FORMAN_TYPE_ALIASES = {
602
+ // Forman's plain-string primitive is `text`; there is no `string` type.
603
+ string: "text",
604
+ bool: "boolean",
605
+ // Forman's `date` already carries time (see `time` on FormanSchemaField, default true).
606
+ datetime: "date",
607
+ // JSON Schema has no float; `number` is the only target.
608
+ float: "number",
609
+ // `upload` is the validator's existing name for the same array-shaped type.
610
+ upload: "filestorage"
611
+ };
612
+ function resolveFormanFieldType(rawType) {
613
+ if (!rawType) return void 0;
614
+ const base = rawType.includes(":") ? rawType.split(":")[0] : rawType;
615
+ const lower = base.toLowerCase();
616
+ return FORMAN_TYPE_LOOKUP.get(lower) ?? FORMAN_TYPE_ALIASES[lower];
617
+ }
618
+ function canonicalTypeWithKind(rawType, canonicalType) {
619
+ const separatorIndex = rawType.indexOf(":");
620
+ return separatorIndex === -1 ? canonicalType : `${canonicalType}${rawType.slice(separatorIndex)}`;
602
621
  }
603
622
  function appendQueryString(path, domain, tail) {
604
623
  if (path.startsWith("api://")) return path;
@@ -616,6 +635,7 @@ function createDefaultContext(options) {
616
635
  roots: {},
617
636
  definitions: {},
618
637
  excludeAdvancedFields: options?.excludeAdvancedFields ?? false,
638
+ strictFieldTypes: options?.strictFieldTypes ?? false,
619
639
  skippedPaths: {},
620
640
  addConditionalFields: () => {
621
641
  throw new SchemaConversionError("Cannot serialize nested fields without parent field.");
@@ -627,8 +647,19 @@ var compositeHandlers = {
627
647
  udttype: { expand: udttypeExpand, extractInner: udttypeExtractInner, wrapRef: udttypeWrapRef }
628
648
  };
629
649
  function toJSONSchemaInternal(field, context) {
630
- validateFormanField(field);
631
- const normalizedField = normalizeFormanFieldType(field);
650
+ const canonicalType = resolveFormanFieldType(field.type);
651
+ if (!canonicalType) {
652
+ if (context.strictFieldTypes) {
653
+ throw new SchemaConversionError(
654
+ field.type ? `Unknown field type: ${field.type}` : "Field type is required",
655
+ field
656
+ );
657
+ }
658
+ return degradeUnconvertibleField(field, context);
659
+ }
660
+ const normalizedField = normalizeFormanFieldType(
661
+ canonicalType === field.type ? field : { ...field, type: canonicalTypeWithKind(field.type, canonicalType) }
662
+ );
632
663
  const handler = compositeHandlers[normalizedField.type];
633
664
  if (handler) {
634
665
  const type = normalizedField.type;
@@ -1043,6 +1074,16 @@ function handleJsonType(field, result) {
1043
1074
  });
1044
1075
  return result;
1045
1076
  }
1077
+ function degradeUnconvertibleField(field, context) {
1078
+ const path = field.name ? [...context.path, field.name].join(".") : context.path.join(".");
1079
+ const reason = field.type ? `unknown type: ${field.type}` : "missing type";
1080
+ (context.skippedPaths.unconvertible ||= []).push(`${path} (${reason})`);
1081
+ return {
1082
+ type: FORMAN_TYPE_MAP["any"],
1083
+ title: noEmpty(field.label),
1084
+ description: noEmpty(field.help)
1085
+ };
1086
+ }
1046
1087
  function handlePrimitiveType(field, result, context) {
1047
1088
  if (field.default !== "" && field.default != null) {
1048
1089
  result.default = field.default;
@@ -1359,6 +1400,19 @@ async function validateFormanValue(value, field, context) {
1359
1400
  warnings: []
1360
1401
  };
1361
1402
  }
1403
+ if (!field.type) {
1404
+ return {
1405
+ valid: false,
1406
+ errors: [
1407
+ {
1408
+ domain: context.domain,
1409
+ path: context.path.join("."),
1410
+ message: "Field type is required."
1411
+ }
1412
+ ],
1413
+ warnings: []
1414
+ };
1415
+ }
1362
1416
  const normalizedField = normalizeFormanFieldType(field);
1363
1417
  if (normalizedField.required && (value == null || value === "")) {
1364
1418
  return {
@@ -2304,6 +2358,9 @@ function toJSONSchemaAdvanced(field, options) {
2304
2358
  if (context.skippedPaths.advanced?.length) {
2305
2359
  skippedPaths.advanced = context.skippedPaths.advanced;
2306
2360
  }
2361
+ if (context.skippedPaths.unconvertible?.length) {
2362
+ skippedPaths.unconvertible = context.skippedPaths.unconvertible;
2363
+ }
2307
2364
  return {
2308
2365
  schema,
2309
2366
  ...Object.keys(skippedPaths).length > 0 ? { skippedPaths } : {}
@@ -2323,6 +2380,8 @@ function validateForman(values, schema, options, restoreExtras) {
2323
2380
  }
2324
2381
  // Annotate the CommonJS export names for ESM import in node:
2325
2382
  0 && (module.exports = {
2383
+ SchemaConversionError,
2384
+ resolveFormanFieldType,
2326
2385
  toFormanSchema,
2327
2386
  toJSONSchema,
2328
2387
  toJSONSchemaAdvanced,
package/dist/index.d.cts CHANGED
@@ -240,6 +240,14 @@ type FormanJsonSchemaOptions = {
240
240
  * `toJSONSchemaAdvanced`'s `skippedPaths.advanced` so the caller can re-request them.
241
241
  */
242
242
  excludeAdvancedFields?: boolean;
243
+ /**
244
+ * Throw a `SchemaConversionError` when a field's type cannot be resolved, instead of degrading
245
+ * it to a permissive typeless schema. Defaults to `false` — by default, unresolvable fields are
246
+ * degraded and their dot-notation paths reported on `toJSONSchemaAdvanced`'s
247
+ * `skippedPaths.unconvertible`, so one unrecognized field can no longer abort the whole
248
+ * conversion. Set `true` only when a caller genuinely wants fail-fast behaviour.
249
+ */
250
+ strictFieldTypes?: boolean;
243
251
  };
244
252
  /**
245
253
  * Result of converting a Forman Schema to JSON Schema
@@ -251,6 +259,12 @@ type FormanJsonSchemaResult = {
251
259
  skippedPaths?: {
252
260
  /** Dot-notation paths of advanced fields that were skipped. Present only when at least one advanced field was skipped. */
253
261
  advanced?: string[];
262
+ /**
263
+ * Dot-notation paths of fields whose type could not be resolved and were therefore degraded
264
+ * to a permissive typeless schema. Each entry is suffixed with the reason —
265
+ * `(unknown type: X)` or `(missing type)`. Present only when at least one field was degraded.
266
+ */
267
+ unconvertible?: string[];
254
268
  };
255
269
  };
256
270
  /**
@@ -294,6 +308,26 @@ type FormanValidationOptions = {
294
308
  */
295
309
  declare function toFormanSchema(field: JSONSchema7): FormanSchemaField;
296
310
 
311
+ /**
312
+ * Error thrown when schema conversion fails
313
+ */
314
+ declare class SchemaConversionError extends Error {
315
+ /** Field that caused the error */
316
+ readonly field?: FormanSchemaField | JSONSchema7;
317
+ /**
318
+ * @param message The error message
319
+ * @param field The field that caused the error
320
+ */
321
+ constructor(message: string, field?: FormanSchemaField | JSONSchema7);
322
+ }
323
+ /**
324
+ * Resolves a raw Forman field type to a canonical `FORMAN_TYPE_MAP` key, handling `type:kind`
325
+ * prefixes, casing and known aliases.
326
+ * @param rawType The raw field type from the Forman schema
327
+ * @returns The canonical type key, or `undefined` when the type is missing or unresolvable
328
+ */
329
+ declare function resolveFormanFieldType(rawType: string | undefined): string | undefined;
330
+
297
331
  /**
298
332
  * Converts a Forman Schema field to its JSON Schema equivalent and reports the paths of any
299
333
  * fields that were skipped during conversion.
@@ -311,6 +345,13 @@ declare function toFormanSchema(field: JSONSchema7): FormanSchemaField;
311
345
  * `definitions[type]`; advanced fields inside a composite template are recorded with the
312
346
  * path of the FIRST usage only. See the comment near `compositeHandlers` in `src/forman.ts`.
313
347
  *
348
+ * **Unresolvable field types** are tolerated by default. A field whose type is missing, or is not a
349
+ * known type (after case-insensitive and alias resolution), is degraded to a permissive typeless
350
+ * schema rather than aborting the whole conversion, and its dot-notation path is reported on
351
+ * `skippedPaths.unconvertible` with the reason. This matters because the throw was fatal at any
352
+ * depth: a single unrecognized leaf field destroyed the entire schema, leaving consumers with
353
+ * nothing. Pass `{ strictFieldTypes: true }` to restore fail-fast throwing.
354
+ *
314
355
  * If you don't need `skippedPaths`, use {@link toJSONSchema} which returns just the schema.
315
356
  *
316
357
  * @param field The Forman Schema field to convert
@@ -326,6 +367,9 @@ declare function toJSONSchemaAdvanced(field: FormanSchemaField, options?: Forman
326
367
  * fields were dropped (e.g. to render a "show advanced" toggle), use {@link toJSONSchemaAdvanced}
327
368
  * which returns `{ schema, skippedPaths? }`.
328
369
  *
370
+ * Fields with an unresolvable type are degraded to a permissive schema rather than throwing; use
371
+ * {@link toJSONSchemaAdvanced} to see which, or `{ strictFieldTypes: true }` to throw instead.
372
+ *
329
373
  * @param field The Forman Schema field to convert
330
374
  * @param options Conversion options
331
375
  * @returns The equivalent JSON Schema
@@ -358,4 +402,4 @@ declare function validateFormanWithDomains(domains: Record<string, {
358
402
  */
359
403
  declare function validateForman(values: Record<string, unknown>, schema: FormanSchemaField[], options?: FormanValidationOptions, restoreExtras?: Record<string, Record<string, unknown>>): Promise<FormanValidationResult>;
360
404
 
361
- export { type FormanExternalValidationResult, type FormanJsonSchemaOptions, type FormanJsonSchemaResult, type FormanSchemaDirectoryOption, type FormanSchemaExtendedNested, type FormanSchemaExtendedOptions, type FormanSchemaField, type FormanSchemaFieldType, type FormanSchemaNested, type FormanSchemaOption, type FormanSchemaOptionGroup, type FormanSchemaPathExtendedOptions, type FormanSchemaRPCButton, type FormanSchemaValue, type FormanValidationOptions, type FormanValidationResult, toFormanSchema, toJSONSchema, toJSONSchemaAdvanced, validateForman, validateFormanWithDomains };
405
+ export { type FormanExternalValidationResult, type FormanJsonSchemaOptions, type FormanJsonSchemaResult, type FormanSchemaDirectoryOption, type FormanSchemaExtendedNested, type FormanSchemaExtendedOptions, type FormanSchemaField, type FormanSchemaFieldType, type FormanSchemaNested, type FormanSchemaOption, type FormanSchemaOptionGroup, type FormanSchemaPathExtendedOptions, type FormanSchemaRPCButton, type FormanSchemaValue, type FormanValidationOptions, type FormanValidationResult, SchemaConversionError, resolveFormanFieldType, toFormanSchema, toJSONSchema, toJSONSchemaAdvanced, validateForman, validateFormanWithDomains };
package/dist/index.d.ts CHANGED
@@ -240,6 +240,14 @@ type FormanJsonSchemaOptions = {
240
240
  * `toJSONSchemaAdvanced`'s `skippedPaths.advanced` so the caller can re-request them.
241
241
  */
242
242
  excludeAdvancedFields?: boolean;
243
+ /**
244
+ * Throw a `SchemaConversionError` when a field's type cannot be resolved, instead of degrading
245
+ * it to a permissive typeless schema. Defaults to `false` — by default, unresolvable fields are
246
+ * degraded and their dot-notation paths reported on `toJSONSchemaAdvanced`'s
247
+ * `skippedPaths.unconvertible`, so one unrecognized field can no longer abort the whole
248
+ * conversion. Set `true` only when a caller genuinely wants fail-fast behaviour.
249
+ */
250
+ strictFieldTypes?: boolean;
243
251
  };
244
252
  /**
245
253
  * Result of converting a Forman Schema to JSON Schema
@@ -251,6 +259,12 @@ type FormanJsonSchemaResult = {
251
259
  skippedPaths?: {
252
260
  /** Dot-notation paths of advanced fields that were skipped. Present only when at least one advanced field was skipped. */
253
261
  advanced?: string[];
262
+ /**
263
+ * Dot-notation paths of fields whose type could not be resolved and were therefore degraded
264
+ * to a permissive typeless schema. Each entry is suffixed with the reason —
265
+ * `(unknown type: X)` or `(missing type)`. Present only when at least one field was degraded.
266
+ */
267
+ unconvertible?: string[];
254
268
  };
255
269
  };
256
270
  /**
@@ -294,6 +308,26 @@ type FormanValidationOptions = {
294
308
  */
295
309
  declare function toFormanSchema(field: JSONSchema7): FormanSchemaField;
296
310
 
311
+ /**
312
+ * Error thrown when schema conversion fails
313
+ */
314
+ declare class SchemaConversionError extends Error {
315
+ /** Field that caused the error */
316
+ readonly field?: FormanSchemaField | JSONSchema7;
317
+ /**
318
+ * @param message The error message
319
+ * @param field The field that caused the error
320
+ */
321
+ constructor(message: string, field?: FormanSchemaField | JSONSchema7);
322
+ }
323
+ /**
324
+ * Resolves a raw Forman field type to a canonical `FORMAN_TYPE_MAP` key, handling `type:kind`
325
+ * prefixes, casing and known aliases.
326
+ * @param rawType The raw field type from the Forman schema
327
+ * @returns The canonical type key, or `undefined` when the type is missing or unresolvable
328
+ */
329
+ declare function resolveFormanFieldType(rawType: string | undefined): string | undefined;
330
+
297
331
  /**
298
332
  * Converts a Forman Schema field to its JSON Schema equivalent and reports the paths of any
299
333
  * fields that were skipped during conversion.
@@ -311,6 +345,13 @@ declare function toFormanSchema(field: JSONSchema7): FormanSchemaField;
311
345
  * `definitions[type]`; advanced fields inside a composite template are recorded with the
312
346
  * path of the FIRST usage only. See the comment near `compositeHandlers` in `src/forman.ts`.
313
347
  *
348
+ * **Unresolvable field types** are tolerated by default. A field whose type is missing, or is not a
349
+ * known type (after case-insensitive and alias resolution), is degraded to a permissive typeless
350
+ * schema rather than aborting the whole conversion, and its dot-notation path is reported on
351
+ * `skippedPaths.unconvertible` with the reason. This matters because the throw was fatal at any
352
+ * depth: a single unrecognized leaf field destroyed the entire schema, leaving consumers with
353
+ * nothing. Pass `{ strictFieldTypes: true }` to restore fail-fast throwing.
354
+ *
314
355
  * If you don't need `skippedPaths`, use {@link toJSONSchema} which returns just the schema.
315
356
  *
316
357
  * @param field The Forman Schema field to convert
@@ -326,6 +367,9 @@ declare function toJSONSchemaAdvanced(field: FormanSchemaField, options?: Forman
326
367
  * fields were dropped (e.g. to render a "show advanced" toggle), use {@link toJSONSchemaAdvanced}
327
368
  * which returns `{ schema, skippedPaths? }`.
328
369
  *
370
+ * Fields with an unresolvable type are degraded to a permissive schema rather than throwing; use
371
+ * {@link toJSONSchemaAdvanced} to see which, or `{ strictFieldTypes: true }` to throw instead.
372
+ *
329
373
  * @param field The Forman Schema field to convert
330
374
  * @param options Conversion options
331
375
  * @returns The equivalent JSON Schema
@@ -358,4 +402,4 @@ declare function validateFormanWithDomains(domains: Record<string, {
358
402
  */
359
403
  declare function validateForman(values: Record<string, unknown>, schema: FormanSchemaField[], options?: FormanValidationOptions, restoreExtras?: Record<string, Record<string, unknown>>): Promise<FormanValidationResult>;
360
404
 
361
- export { type FormanExternalValidationResult, type FormanJsonSchemaOptions, type FormanJsonSchemaResult, type FormanSchemaDirectoryOption, type FormanSchemaExtendedNested, type FormanSchemaExtendedOptions, type FormanSchemaField, type FormanSchemaFieldType, type FormanSchemaNested, type FormanSchemaOption, type FormanSchemaOptionGroup, type FormanSchemaPathExtendedOptions, type FormanSchemaRPCButton, type FormanSchemaValue, type FormanValidationOptions, type FormanValidationResult, toFormanSchema, toJSONSchema, toJSONSchemaAdvanced, validateForman, validateFormanWithDomains };
405
+ export { type FormanExternalValidationResult, type FormanJsonSchemaOptions, type FormanJsonSchemaResult, type FormanSchemaDirectoryOption, type FormanSchemaExtendedNested, type FormanSchemaExtendedOptions, type FormanSchemaField, type FormanSchemaFieldType, type FormanSchemaNested, type FormanSchemaOption, type FormanSchemaOptionGroup, type FormanSchemaPathExtendedOptions, type FormanSchemaRPCButton, type FormanSchemaValue, type FormanValidationOptions, type FormanValidationResult, SchemaConversionError, resolveFormanFieldType, toFormanSchema, toJSONSchema, toJSONSchemaAdvanced, validateForman, validateFormanWithDomains };
package/dist/index.js CHANGED
@@ -513,11 +513,13 @@ var SchemaConversionError = class extends Error {
513
513
  constructor(message, field) {
514
514
  super(message);
515
515
  this.name = "SchemaConversionError";
516
+ this.field = field;
516
517
  }
517
518
  };
518
519
  var FORMAN_TYPE_MAP = {
519
520
  account: "number",
520
521
  hook: "number",
522
+ device: "number",
521
523
  keychain: "number",
522
524
  datastore: "number",
523
525
  aiagent: "string",
@@ -561,14 +563,29 @@ var FORMAN_TYPE_MAP = {
561
563
  uuid: "string",
562
564
  any: void 0
563
565
  };
564
- function validateFormanField(field) {
565
- if (!field.type) {
566
- throw new SchemaConversionError("Field type is required", field);
567
- }
568
- const normalizedType = field.type.includes(":") ? field.type.split(":")[0] : field.type;
569
- if (!Object.keys(FORMAN_TYPE_MAP).includes(normalizedType)) {
570
- throw new SchemaConversionError(`Unknown field type: ${field.type}`, field);
571
- }
566
+ var FORMAN_TYPE_LOOKUP = new Map(
567
+ Object.keys(FORMAN_TYPE_MAP).map((key) => [key.toLowerCase(), key])
568
+ );
569
+ var FORMAN_TYPE_ALIASES = {
570
+ // Forman's plain-string primitive is `text`; there is no `string` type.
571
+ string: "text",
572
+ bool: "boolean",
573
+ // Forman's `date` already carries time (see `time` on FormanSchemaField, default true).
574
+ datetime: "date",
575
+ // JSON Schema has no float; `number` is the only target.
576
+ float: "number",
577
+ // `upload` is the validator's existing name for the same array-shaped type.
578
+ upload: "filestorage"
579
+ };
580
+ function resolveFormanFieldType(rawType) {
581
+ if (!rawType) return void 0;
582
+ const base = rawType.includes(":") ? rawType.split(":")[0] : rawType;
583
+ const lower = base.toLowerCase();
584
+ return FORMAN_TYPE_LOOKUP.get(lower) ?? FORMAN_TYPE_ALIASES[lower];
585
+ }
586
+ function canonicalTypeWithKind(rawType, canonicalType) {
587
+ const separatorIndex = rawType.indexOf(":");
588
+ return separatorIndex === -1 ? canonicalType : `${canonicalType}${rawType.slice(separatorIndex)}`;
572
589
  }
573
590
  function appendQueryString(path, domain, tail) {
574
591
  if (path.startsWith("api://")) return path;
@@ -586,6 +603,7 @@ function createDefaultContext(options) {
586
603
  roots: {},
587
604
  definitions: {},
588
605
  excludeAdvancedFields: options?.excludeAdvancedFields ?? false,
606
+ strictFieldTypes: options?.strictFieldTypes ?? false,
589
607
  skippedPaths: {},
590
608
  addConditionalFields: () => {
591
609
  throw new SchemaConversionError("Cannot serialize nested fields without parent field.");
@@ -597,8 +615,19 @@ var compositeHandlers = {
597
615
  udttype: { expand: udttypeExpand, extractInner: udttypeExtractInner, wrapRef: udttypeWrapRef }
598
616
  };
599
617
  function toJSONSchemaInternal(field, context) {
600
- validateFormanField(field);
601
- const normalizedField = normalizeFormanFieldType(field);
618
+ const canonicalType = resolveFormanFieldType(field.type);
619
+ if (!canonicalType) {
620
+ if (context.strictFieldTypes) {
621
+ throw new SchemaConversionError(
622
+ field.type ? `Unknown field type: ${field.type}` : "Field type is required",
623
+ field
624
+ );
625
+ }
626
+ return degradeUnconvertibleField(field, context);
627
+ }
628
+ const normalizedField = normalizeFormanFieldType(
629
+ canonicalType === field.type ? field : { ...field, type: canonicalTypeWithKind(field.type, canonicalType) }
630
+ );
602
631
  const handler = compositeHandlers[normalizedField.type];
603
632
  if (handler) {
604
633
  const type = normalizedField.type;
@@ -1013,6 +1042,16 @@ function handleJsonType(field, result) {
1013
1042
  });
1014
1043
  return result;
1015
1044
  }
1045
+ function degradeUnconvertibleField(field, context) {
1046
+ const path = field.name ? [...context.path, field.name].join(".") : context.path.join(".");
1047
+ const reason = field.type ? `unknown type: ${field.type}` : "missing type";
1048
+ (context.skippedPaths.unconvertible ||= []).push(`${path} (${reason})`);
1049
+ return {
1050
+ type: FORMAN_TYPE_MAP["any"],
1051
+ title: noEmpty(field.label),
1052
+ description: noEmpty(field.help)
1053
+ };
1054
+ }
1016
1055
  function handlePrimitiveType(field, result, context) {
1017
1056
  if (field.default !== "" && field.default != null) {
1018
1057
  result.default = field.default;
@@ -1329,6 +1368,19 @@ async function validateFormanValue(value, field, context) {
1329
1368
  warnings: []
1330
1369
  };
1331
1370
  }
1371
+ if (!field.type) {
1372
+ return {
1373
+ valid: false,
1374
+ errors: [
1375
+ {
1376
+ domain: context.domain,
1377
+ path: context.path.join("."),
1378
+ message: "Field type is required."
1379
+ }
1380
+ ],
1381
+ warnings: []
1382
+ };
1383
+ }
1332
1384
  const normalizedField = normalizeFormanFieldType(field);
1333
1385
  if (normalizedField.required && (value == null || value === "")) {
1334
1386
  return {
@@ -2274,6 +2326,9 @@ function toJSONSchemaAdvanced(field, options) {
2274
2326
  if (context.skippedPaths.advanced?.length) {
2275
2327
  skippedPaths.advanced = context.skippedPaths.advanced;
2276
2328
  }
2329
+ if (context.skippedPaths.unconvertible?.length) {
2330
+ skippedPaths.unconvertible = context.skippedPaths.unconvertible;
2331
+ }
2277
2332
  return {
2278
2333
  schema,
2279
2334
  ...Object.keys(skippedPaths).length > 0 ? { skippedPaths } : {}
@@ -2292,6 +2347,8 @@ function validateForman(values, schema, options, restoreExtras) {
2292
2347
  );
2293
2348
  }
2294
2349
  export {
2350
+ SchemaConversionError,
2351
+ resolveFormanFieldType,
2295
2352
  toFormanSchema,
2296
2353
  toJSONSchema,
2297
2354
  toJSONSchemaAdvanced,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makehq/forman-schema",
3
- "version": "1.16.0",
3
+ "version": "1.17.0",
4
4
  "description": "Forman Schema Tools",
5
5
  "license": "MIT",
6
6
  "author": "Make",