A flexible and powerful path normalization utility for mapping backend error paths to frontend form fields. Perfect for handling complex form validation scenarios where backend and frontend field structures don't match.
- π― Flexible Pattern Matching: Support for exact matches, wildcards, regex, and custom matchers
- π³ Tree-like Structure: Efficient matching with support for nested paths and deep wildcards
- π Transform Functions: Powerful transformation capabilities for complex path mappings
- π¦ TypeScript Support: Full type safety and IntelliSense support
- π¨ Builder Pattern: Intuitive API for creating rules
- β‘ Performance Optimized: Priority-based rule evaluation for optimal performance
- π§ Framework Agnostic: Works with any form library (react-hook-form, formik, etc.)
npm install path-normalizer
# or
yarn add path-normalizer
# or
pnpm add path-normalizerimport { PathNormalizer, PathRuleBuilder } from 'path-normalizer';
// Create a normalizer instance
const normalizer = new PathNormalizer({
delimiter: '.',
caseInsensitive: false
});
// Add a simple rule
normalizer.addRule(
['user', 'email'],
(matched) => ['profile', 'contact', 'email']
);
// Normalize a path
const result = normalizer.normalizePath('user.email');
console.log(result.normalized); // 'profile.contact.email'// Single wildcard - matches any single segment
normalizer.addRule(
['users', '*', 'email'],
(matched, context) => {
const userId = context.segments[1];
return `users[${userId}].emailAddress`;
}
);
// Deep wildcard - matches any remaining segments
normalizer.addRule(
['api', 'v1', '**'],
(matched, context) => {
const remainingPath = context.segments.slice(2);
return ['internal', 'api', ...remainingPath];
}
);const rule = PathRuleBuilder.create()
.exact('text_templates')
.oneOf(['en', 'ua', 'az']) // Match any of these locales
.deepWildcard() // Match any nested path
.transform((matched, context) => {
const locale = context.segments[1];
const localeIndex = ['en', 'ua', 'az'].indexOf(locale);
const remainingPath = context.segments.slice(2);
return [
'localization_fields',
localeIndex.toString(),
'text_templates',
...remainingPath
];
})
.setPriority(10) // Higher priority rules are evaluated first
.build();
normalizer.addRules([rule]);import { useEffect, useMemo } from 'react';
import { UseFormReturn } from 'react-hook-form';
import { PathNormalizer } from 'path-normalizer';
export function useNormalizedFormErrors({
methods,
backendErrors,
normalizer
}) {
const { setError, clearErrors } = methods;
useEffect(() => {
if (!backendErrors) return;
clearErrors();
Object.entries(backendErrors).forEach(([path, message]) => {
const result = normalizer.normalizePath(path);
if (result.matched && result.normalized) {
setError(result.normalized, {
type: 'manual',
message
});
}
});
}, [backendErrors, normalizer, setError, clearErrors]);
}Handling complex backend error paths in a multi-language form:
// Backend sends: text_templates['en'].text
// Frontend needs: localization_fields.0.text_templates.text
const normalizer = new PathNormalizer();
const allowedLocales = ['en', 'ua', 'az'];
normalizer.addRule(
['text_templates', (seg) => allowedLocales.includes(seg), '**'],
(matched, context) => {
const locale = context.segments[1];
const localeIndex = allowedLocales.indexOf(locale);
const remainingPath = context.segments.slice(2);
return [
'localization_fields',
localeIndex.toString(),
'text_templates',
...remainingPath
].join('.');
},
10 // High priority
);
// Test it
const paths = [
"text_templates['en'].text",
"text_templates['en'].variables[0]",
"text_templates['az'].settings.enabled"
];
paths.forEach(path => {
const result = normalizer.normalizePath(path);
console.log(`${path} -> ${result.normalized}`);
});
// Output:
// text_templates['en'].text -> localization_fields.0.text_templates.text
// text_templates['en'].variables[0] -> localization_fields.0.text_templates.variables.0
// text_templates['az'].settings.enabled -> localization_fields.2.text_templates.settings.enabledinterface PathNormalizerOptions {
delimiter?: string; // Path delimiter (default: '.')
caseInsensitive?: boolean; // Case-insensitive matching (default: false)
throwOnUnmatched?: boolean; // Throw error for unmatched paths (default: false)
}addRule(pattern, transform, priority?): Add a normalization ruleaddRules(rules): Add multiple rules at oncenormalizePath(path): Normalize a single pathnormalizePaths(paths): Normalize multiple pathsclearRules(): Remove all rules
Fluent API for building rules:
exact(segment): Match exact segmentregex(pattern): Match with regexwildcard(): Match any single segmentdeepWildcard(): Match any remaining segmentsoneOf(values): Match any of the provided valuescustom(matcher): Custom matcher functiontransform(fn): Set transformation functionsetPriority(n): Set rule prioritybuild(): Build the rule
Helper functions for common transformations:
replaceSegment(index, newValue): Replace a segment at indexprefix(...segments): Add prefix segmentsmapTo(template, replacements): Map to template with replacementsremove(): Remove the path entirely
| Feature | path-normalizer | path-to-regexp |
|---|---|---|
| Wildcard support | β | β |
| Custom matchers | β | β |
| Tree-like patterns | β | β |
| TypeScript | β | β |
-
Designed for Real-World Form Validation: Built specifically for the common problem of mapping backend validation errors to frontend form fields
-
Flexible Pattern Matching: Unlike simple string replacement, supports complex patterns including wildcards, regex, and custom matchers
-
Performance Optimized: Priority-based evaluation ensures the most likely rules are checked first
-
Type-Safe: Full TypeScript support with proper type inference
-
Framework Agnostic
See CHANGELOG.md for a detailed history of changes and updates.