diff --git a/examples/app/comprehensive-validation.json b/examples/app/comprehensive-validation.json
new file mode 100644
index 0000000..0a4b792
--- /dev/null
+++ b/examples/app/comprehensive-validation.json
@@ -0,0 +1,681 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "type": "object",
+ "title": "Comprehensive Validation Demo",
+ "description": "A complete example showcasing all JSON Schema 2020-12 validation features",
+ "properties": {
+ "personalInfo": {
+ "type": "object",
+ "title": "Personal Information",
+ "properties": {
+ "firstName": {
+ "type": "string",
+ "title": "First Name",
+ "minLength": 2,
+ "maxLength": 50,
+ "pattern": "^[A-Za-z\\s]+$",
+ "ui": {
+ "element": "input",
+ "type": "text",
+ "class": "form-group",
+ "order": 1,
+ "placeholder": "Enter your first name"
+ }
+ },
+ "lastName": {
+ "type": "string",
+ "title": "Last Name",
+ "minLength": 2,
+ "maxLength": 50,
+ "pattern": "^[A-Za-z\\s]+$",
+ "ui": {
+ "element": "input",
+ "type": "text",
+ "class": "form-group",
+ "order": 2
+ }
+ },
+ "age": {
+ "type": "integer",
+ "title": "Age",
+ "minimum": 18,
+ "maximum": 120,
+ "ui": {
+ "element": "input",
+ "type": "number",
+ "class": "form-group",
+ "order": 3
+ }
+ },
+ "email": {
+ "type": "string",
+ "format": "email",
+ "title": "Email Address",
+ "ui": {
+ "element": "input",
+ "type": "email",
+ "class": "form-group",
+ "order": 4
+ }
+ },
+ "phone": {
+ "type": "string",
+ "title": "Phone Number",
+ "pattern": "^\\+?[1-9]\\d{1,14}$",
+ "ui": {
+ "element": "input",
+ "type": "tel",
+ "class": "form-group",
+ "order": 5
+ }
+ },
+ "dateOfBirth": {
+ "type": "string",
+ "format": "date",
+ "title": "Date of Birth",
+ "ui": {
+ "element": "input",
+ "type": "date",
+ "class": "form-group",
+ "order": 6
+ }
+ },
+ "website": {
+ "type": "string",
+ "format": "uri",
+ "title": "Personal Website",
+ "ui": {
+ "element": "input",
+ "type": "url",
+ "class": "form-group",
+ "order": 7
+ }
+ }
+ },
+ "required": [ "firstName", "lastName", "email" ],
+ "dependentRequired": {
+ "phone": [ "email" ],
+ "website": [ "email" ]
+ }
+ },
+ "address": {
+ "type": "object",
+ "title": "Address Information",
+ "properties": {
+ "street": {
+ "type": "string",
+ "title": "Street Address",
+ "minLength": 5,
+ "maxLength": 100,
+ "ui": {
+ "element": "input",
+ "type": "text",
+ "class": "form-group",
+ "order": 1
+ }
+ },
+ "city": {
+ "type": "string",
+ "title": "City",
+ "minLength": 2,
+ "maxLength": 50,
+ "ui": {
+ "element": "input",
+ "type": "text",
+ "class": "form-group",
+ "order": 2
+ }
+ },
+ "state": {
+ "type": "string",
+ "title": "State/Province",
+ "ui": {
+ "element": "input",
+ "type": "text",
+ "class": "form-group",
+ "order": 3
+ }
+ },
+ "country": {
+ "type": "string",
+ "title": "Country",
+ "enum": [ "US", "CA", "UK", "DE", "FR", "AU", "JP", "IN" ],
+ "ui": {
+ "element": "select",
+ "class": "form-group",
+ "order": 4,
+ "options": [
+ { "value": "US", "text": "United States" },
+ { "value": "CA", "text": "Canada" },
+ { "value": "UK", "text": "United Kingdom" },
+ { "value": "DE", "text": "Germany" },
+ { "value": "FR", "text": "France" },
+ { "value": "AU", "text": "Australia" },
+ { "value": "JP", "text": "Japan" },
+ { "value": "IN", "text": "India" }
+ ]
+ }
+ },
+ "postalCode": {
+ "type": "string",
+ "title": "Postal Code",
+ "ui": {
+ "element": "input",
+ "type": "text",
+ "class": "form-group",
+ "order": 5
+ }
+ }
+ },
+ "required": [ "street", "city", "country" ],
+ "allOf": [
+ {
+ "if": {
+ "properties": {
+ "country": { "const": "US" }
+ }
+ },
+ "then": {
+ "properties": {
+ "postalCode": {
+ "pattern": "^\\d{5}(-\\d{4})?$"
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "properties": {
+ "country": { "const": "CA" }
+ }
+ },
+ "then": {
+ "properties": {
+ "postalCode": {
+ "pattern": "^[A-Z]\\d[A-Z] \\d[A-Z]\\d$"
+ }
+ }
+ }
+ }
+ ]
+ },
+ "preferences": {
+ "type": "object",
+ "title": "User Preferences",
+ "properties": {
+ "newsletter": {
+ "type": "boolean",
+ "title": "Subscribe to Newsletter",
+ "default": false,
+ "ui": {
+ "element": "input",
+ "type": "checkbox",
+ "class": "form-group",
+ "order": 1
+ }
+ },
+ "notifications": {
+ "type": "array",
+ "title": "Notification Types",
+ "minItems": 1,
+ "maxItems": 5,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "enum": [ "email", "sms", "push", "phone", "mail" ]
+ },
+ "ui": {
+ "element": "select",
+ "class": "form-group",
+ "order": 2,
+ "multiple": true,
+ "options": [
+ { "value": "email", "text": "Email Notifications" },
+ { "value": "sms", "text": "SMS Notifications" },
+ { "value": "push", "text": "Push Notifications" },
+ { "value": "phone", "text": "Phone Calls" },
+ { "value": "mail", "text": "Physical Mail" }
+ ]
+ }
+ },
+ "theme": {
+ "type": "string",
+ "title": "Preferred Theme",
+ "enum": [ "light", "dark", "auto" ],
+ "default": "auto",
+ "ui": {
+ "element": "select",
+ "class": "form-group",
+ "order": 3,
+ "options": [
+ { "value": "light", "text": "Light Theme" },
+ { "value": "dark", "text": "Dark Theme" },
+ { "value": "auto", "text": "Auto (System)" }
+ ]
+ }
+ },
+ "language": {
+ "type": "string",
+ "title": "Preferred Language",
+ "enum": [ "en", "es", "fr", "de", "zh", "ja" ],
+ "default": "en",
+ "ui": {
+ "element": "select",
+ "class": "form-group",
+ "order": 4,
+ "options": [
+ { "value": "en", "text": "English" },
+ { "value": "es", "text": "Español" },
+ { "value": "fr", "text": "Français" },
+ { "value": "de", "text": "Deutsch" },
+ { "value": "zh", "text": "中文" },
+ { "value": "ja", "text": "日本語" }
+ ]
+ }
+ }
+ }
+ },
+ "financial": {
+ "type": "object",
+ "title": "Financial Information",
+ "properties": {
+ "annualIncome": {
+ "type": "number",
+ "title": "Annual Income",
+ "minimum": 0,
+ "maximum": 10000000,
+ "multipleOf": 1000,
+ "ui": {
+ "element": "input",
+ "type": "number",
+ "class": "form-group",
+ "order": 1,
+ "step": "1000"
+ }
+ },
+ "creditScore": {
+ "type": "integer",
+ "title": "Credit Score",
+ "minimum": 300,
+ "maximum": 850,
+ "ui": {
+ "element": "input",
+ "type": "range",
+ "class": "form-group",
+ "order": 2,
+ "min": "300",
+ "max": "850"
+ }
+ },
+ "accountType": {
+ "type": "string",
+ "title": "Account Type",
+ "oneOf": [
+ {
+ "const": "basic",
+ "title": "Basic Account",
+ "description": "Free account with basic features"
+ },
+ {
+ "const": "premium",
+ "title": "Premium Account",
+ "description": "Paid account with advanced features"
+ },
+ {
+ "const": "enterprise",
+ "title": "Enterprise Account",
+ "description": "Business account with full features"
+ }
+ ],
+ "ui": {
+ "element": "select",
+ "class": "form-group",
+ "order": 3,
+ "options": [
+ { "value": "basic", "text": "Basic Account" },
+ { "value": "premium", "text": "Premium Account" },
+ { "value": "enterprise", "text": "Enterprise Account" }
+ ]
+ }
+ }
+ },
+ "if": {
+ "properties": {
+ "accountType": { "const": "premium" }
+ }
+ },
+ "then": {
+ "properties": {
+ "paymentMethod": {
+ "type": "string",
+ "title": "Payment Method",
+ "enum": [ "credit_card", "debit_card", "paypal", "bank_transfer" ],
+ "ui": {
+ "element": "select",
+ "class": "form-group",
+ "order": 4,
+ "options": [
+ { "value": "credit_card", "text": "Credit Card" },
+ { "value": "debit_card", "text": "Debit Card" },
+ { "value": "paypal", "text": "PayPal" },
+ { "value": "bank_transfer", "text": "Bank Transfer" }
+ ]
+ }
+ }
+ },
+ "required": [ "paymentMethod" ]
+ },
+ "else": {
+ "not": {
+ "required": [ "paymentMethod" ]
+ }
+ }
+ },
+ "skills": {
+ "type": "array",
+ "title": "Technical Skills",
+ "minItems": 1,
+ "maxItems": 10,
+ "uniqueItems": true,
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "title": "Skill Name",
+ "minLength": 2,
+ "maxLength": 50
+ },
+ "level": {
+ "type": "string",
+ "title": "Proficiency Level",
+ "enum": [ "beginner", "intermediate", "advanced", "expert" ]
+ },
+ "yearsExperience": {
+ "type": "number",
+ "title": "Years of Experience",
+ "minimum": 0,
+ "maximum": 50,
+ "multipleOf": 0.5
+ }
+ },
+ "required": [ "name", "level" ]
+ },
+ "ui": {
+ "element": "fieldset",
+ "class": "skills-section",
+ "order": 1
+ }
+ },
+ "portfolio": {
+ "type": "object",
+ "title": "Portfolio",
+ "properties": {
+ "projects": {
+ "type": "array",
+ "title": "Projects",
+ "maxItems": 5,
+ "items": {
+ "type": "object",
+ "properties": {
+ "title": {
+ "type": "string",
+ "title": "Project Title",
+ "minLength": 3,
+ "maxLength": 100
+ },
+ "description": {
+ "type": "string",
+ "title": "Description",
+ "maxLength": 500,
+ "ui": {
+ "element": "textarea",
+ "rows": 4,
+ "class": "form-group"
+ }
+ },
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "title": "Project URL"
+ },
+ "technologies": {
+ "type": "array",
+ "title": "Technologies Used",
+ "items": {
+ "type": "string"
+ },
+ "minItems": 1,
+ "maxItems": 10,
+ "uniqueItems": true
+ }
+ },
+ "required": [ "title", "description" ]
+ }
+ }
+ },
+ "anyOf": [
+ {
+ "properties": {
+ "githubUrl": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https://github\\.com/",
+ "title": "GitHub Profile"
+ }
+ }
+ },
+ {
+ "properties": {
+ "linkedinUrl": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https://www\\.linkedin\\.com/",
+ "title": "LinkedIn Profile"
+ }
+ }
+ },
+ {
+ "properties": {
+ "personalWebsite": {
+ "type": "string",
+ "format": "uri",
+ "title": "Personal Website"
+ }
+ }
+ }
+ ]
+ },
+ "comments": {
+ "type": "string",
+ "title": "Additional Comments",
+ "maxLength": 1000,
+ "ui": {
+ "element": "textarea",
+ "class": "form-group",
+ "rows": 5,
+ "placeholder": "Any additional information you'd like to share..."
+ }
+ },
+ "termsAccepted": {
+ "type": "boolean",
+ "title": "I accept the terms and conditions",
+ "const": true,
+ "ui": {
+ "element": "input",
+ "type": "checkbox",
+ "class": "form-group required-checkbox"
+ }
+ },
+ "privacyAccepted": {
+ "type": "boolean",
+ "title": "I accept the privacy policy",
+ "const": true,
+ "ui": {
+ "element": "input",
+ "type": "checkbox",
+ "class": "form-group required-checkbox"
+ }
+ }
+ },
+ "required": [ "personalInfo", "address", "termsAccepted", "privacyAccepted" ],
+ "dependentSchemas": {
+ "financial": {
+ "properties": {
+ "personalInfo": {
+ "properties": {
+ "age": {
+ "minimum": 21
+ }
+ }
+ }
+ }
+ }
+ },
+ "form": {
+ "class": "comprehensive-form",
+ "action": "/api/submit-comprehensive-form",
+ "method": "POST",
+ "enctype": "multipart/form-data",
+ "groups": [
+ {
+ "title": {
+ "text": "Personal Information",
+ "class": "section-header"
+ },
+ "class": "personal-info-section",
+ "fields": [
+ "personalInfo.firstName",
+ "personalInfo.lastName",
+ "personalInfo.age",
+ "personalInfo.email",
+ "personalInfo.phone",
+ "personalInfo.dateOfBirth",
+ "personalInfo.website"
+ ]
+ },
+ {
+ "title": {
+ "text": "Address Information",
+ "class": "section-header"
+ },
+ "class": "address-section flex flex-wrap gap-4",
+ "fields": [
+ "address.street",
+ "address.city",
+ "address.state",
+ "address.country",
+ "address.postalCode"
+ ]
+ },
+ {
+ "title": {
+ "text": "Preferences & Settings",
+ "class": "section-header"
+ },
+ "class": "preferences-section",
+ "fields": [
+ "preferences.newsletter",
+ "preferences.notifications",
+ "preferences.theme",
+ "preferences.language"
+ ]
+ },
+ {
+ "title": {
+ "text": "Financial Information",
+ "class": "section-header"
+ },
+ "class": "financial-section",
+ "fields": [
+ "financial.annualIncome",
+ "financial.creditScore",
+ "financial.accountType"
+ ]
+ },
+ {
+ "title": {
+ "text": "Skills & Portfolio",
+ "class": "section-header"
+ },
+ "class": "skills-section",
+ "fields": [
+ "skills",
+ "portfolio"
+ ]
+ },
+ {
+ "title": {
+ "text": "Final Details",
+ "class": "section-header"
+ },
+ "class": "final-section",
+ "fields": [
+ "comments",
+ "termsAccepted",
+ "privacyAccepted"
+ ]
+ }
+ ],
+ "submit": {
+ "label": "Submit Application",
+ "class": "btn btn-primary btn-lg",
+ "id": "submit-btn"
+ },
+ "reset": {
+ "label": "Reset Form",
+ "class": "btn btn-secondary",
+ "id": "reset-btn"
+ },
+ "buttons": [
+ {
+ "type": "button",
+ "label": "Save Draft",
+ "class": "btn btn-outline-secondary",
+ "id": "save-draft-btn",
+ "onclick": "saveDraft()"
+ },
+ {
+ "type": "button",
+ "label": "Preview",
+ "class": "btn btn-outline-primary",
+ "id": "preview-btn",
+ "onclick": "showPreview()"
+ }
+ ]
+ },
+ "examples": [
+ {
+ "personalInfo": {
+ "firstName": "John",
+ "lastName": "Doe",
+ "age": 30,
+ "email": "john.doe@example.com",
+ "phone": "+1-555-123-4567",
+ "dateOfBirth": "1993-05-15",
+ "website": "https://johndoe.com"
+ },
+ "address": {
+ "street": "123 Main St",
+ "city": "New York",
+ "state": "NY",
+ "country": "US",
+ "postalCode": "10001"
+ },
+ "preferences": {
+ "newsletter": true,
+ "notifications": [ "email", "push" ],
+ "theme": "dark",
+ "language": "en"
+ },
+ "financial": {
+ "annualIncome": 75000,
+ "creditScore": 750,
+ "accountType": "premium",
+ "paymentMethod": "credit_card"
+ },
+ "termsAccepted": true,
+ "privacyAccepted": true
+ }
+ ]
+}
diff --git a/examples/app/validation-features.json b/examples/app/validation-features.json
new file mode 100644
index 0000000..81dcc9e
--- /dev/null
+++ b/examples/app/validation-features.json
@@ -0,0 +1,586 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "title": "Advanced Validation Features Demo",
+ "description": "Demonstrates all implemented validation features",
+ "properties": {
+ "stringValidations": {
+ "type": "object",
+ "title": "String Validation Examples",
+ "properties": {
+ "basicString": {
+ "type": "string",
+ "title": "Basic String with Length Limits",
+ "minLength": 3,
+ "maxLength": 20,
+ "ui": {
+ "element": "input",
+ "type": "text",
+ "class": "form-group",
+ "order": 1
+ }
+ },
+ "patternString": {
+ "type": "string",
+ "title": "Pattern Validation (Letters Only)",
+ "pattern": "^[A-Za-z\\s]+$",
+ "ui": {
+ "element": "input",
+ "type": "text",
+ "class": "form-group",
+ "order": 2,
+ "placeholder": "Letters and spaces only"
+ }
+ },
+ "emailField": {
+ "type": "string",
+ "format": "email",
+ "title": "Email Format Validation",
+ "ui": {
+ "element": "input",
+ "type": "email",
+ "class": "form-group",
+ "order": 3
+ }
+ },
+ "urlField": {
+ "type": "string",
+ "format": "uri",
+ "title": "URL Format Validation",
+ "ui": {
+ "element": "input",
+ "type": "url",
+ "class": "form-group",
+ "order": 4
+ }
+ },
+ "dateField": {
+ "type": "string",
+ "format": "date",
+ "title": "Date Format Validation",
+ "ui": {
+ "element": "input",
+ "type": "date",
+ "class": "form-group",
+ "order": 5
+ }
+ },
+ "enumField": {
+ "type": "string",
+ "title": "Enum Selection",
+ "enum": [ "option1", "option2", "option3" ],
+ "ui": {
+ "element": "select",
+ "class": "form-group",
+ "order": 6,
+ "options": [
+ { "value": "option1", "text": "Option 1" },
+ { "value": "option2", "text": "Option 2" },
+ { "value": "option3", "text": "Option 3" }
+ ]
+ }
+ }
+ },
+ "required": [ "basicString", "emailField" ]
+ },
+ "numericValidations": {
+ "type": "object",
+ "title": "Numeric Validation Examples",
+ "properties": {
+ "integerField": {
+ "type": "integer",
+ "title": "Integer with Range",
+ "minimum": 1,
+ "maximum": 100,
+ "ui": {
+ "element": "input",
+ "type": "number",
+ "class": "form-group",
+ "order": 1
+ }
+ },
+ "numberField": {
+ "type": "number",
+ "title": "Number with Exclusive Range",
+ "exclusiveMinimum": 0,
+ "exclusiveMaximum": 1000,
+ "multipleOf": 0.5,
+ "ui": {
+ "element": "input",
+ "type": "number",
+ "class": "form-group",
+ "order": 2,
+ "step": "0.5"
+ }
+ },
+ "rangeField": {
+ "type": "integer",
+ "title": "Range Slider",
+ "minimum": 0,
+ "maximum": 100,
+ "default": 50,
+ "ui": {
+ "element": "input",
+ "type": "range",
+ "class": "form-group",
+ "order": 3
+ }
+ }
+ }
+ },
+ "arrayValidations": {
+ "type": "object",
+ "title": "Array Validation Examples",
+ "properties": {
+ "multiSelect": {
+ "type": "array",
+ "title": "Multi-Select with Constraints",
+ "minItems": 2,
+ "maxItems": 4,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "enum": [ "red", "green", "blue", "yellow", "purple", "orange" ]
+ },
+ "ui": {
+ "element": "select",
+ "class": "form-group",
+ "order": 1,
+ "multiple": true,
+ "options": [
+ { "value": "red", "text": "Red" },
+ { "value": "green", "text": "Green" },
+ { "value": "blue", "text": "Blue" },
+ { "value": "yellow", "text": "Yellow" },
+ { "value": "purple", "text": "Purple" },
+ { "value": "orange", "text": "Orange" }
+ ]
+ }
+ },
+ "dynamicList": {
+ "type": "array",
+ "title": "Dynamic List of Objects",
+ "minItems": 1,
+ "maxItems": 5,
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "minLength": 2
+ },
+ "value": {
+ "type": "number",
+ "title": "Value",
+ "minimum": 0
+ }
+ },
+ "required": [ "name" ]
+ },
+ "ui": {
+ "element": "fieldset",
+ "class": "dynamic-list",
+ "order": 2
+ }
+ }
+ }
+ },
+ "conditionalValidations": {
+ "type": "object",
+ "title": "Conditional Validation Examples",
+ "properties": {
+ "userType": {
+ "type": "string",
+ "title": "User Type",
+ "enum": [ "individual", "business", "non-profit" ],
+ "ui": {
+ "element": "select",
+ "class": "form-group",
+ "order": 1,
+ "options": [
+ { "value": "individual", "text": "Individual" },
+ { "value": "business", "text": "Business" },
+ { "value": "non-profit", "text": "Non-Profit" }
+ ]
+ }
+ },
+ "individualInfo": {
+ "type": "object",
+ "title": "Individual Information",
+ "properties": {
+ "ssn": {
+ "type": "string",
+ "title": "Social Security Number",
+ "pattern": "^\\d{3}-\\d{2}-\\d{4}$",
+ "ui": {
+ "element": "input",
+ "type": "text",
+ "class": "form-group",
+ "placeholder": "XXX-XX-XXXX"
+ }
+ }
+ }
+ },
+ "businessInfo": {
+ "type": "object",
+ "title": "Business Information",
+ "properties": {
+ "taxId": {
+ "type": "string",
+ "title": "Tax ID",
+ "pattern": "^\\d{2}-\\d{7}$",
+ "ui": {
+ "element": "input",
+ "type": "text",
+ "class": "form-group",
+ "placeholder": "XX-XXXXXXX"
+ }
+ },
+ "businessName": {
+ "type": "string",
+ "title": "Business Name",
+ "minLength": 2,
+ "ui": {
+ "element": "input",
+ "type": "text",
+ "class": "form-group"
+ }
+ }
+ }
+ }
+ },
+ "allOf": [
+ {
+ "if": {
+ "properties": {
+ "userType": { "const": "individual" }
+ }
+ },
+ "then": {
+ "properties": {
+ "individualInfo": {
+ "required": [ "ssn" ]
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "properties": {
+ "userType": { "const": "business" }
+ }
+ },
+ "then": {
+ "properties": {
+ "businessInfo": {
+ "required": [ "taxId", "businessName" ]
+ }
+ }
+ }
+ }
+ ]
+ },
+ "dependentFields": {
+ "type": "object",
+ "title": "Dependent Field Examples",
+ "properties": {
+ "hasPhone": {
+ "type": "boolean",
+ "title": "I have a phone number",
+ "ui": {
+ "element": "input",
+ "type": "checkbox",
+ "class": "form-group",
+ "order": 1
+ }
+ },
+ "phoneNumber": {
+ "type": "string",
+ "title": "Phone Number",
+ "pattern": "^\\+?[1-9]\\d{1,14}$",
+ "ui": {
+ "element": "input",
+ "type": "tel",
+ "class": "form-group",
+ "order": 2
+ }
+ },
+ "preferredContact": {
+ "type": "string",
+ "title": "Preferred Contact Method",
+ "enum": [ "email", "phone", "mail" ],
+ "ui": {
+ "element": "select",
+ "class": "form-group",
+ "order": 3,
+ "options": [
+ { "value": "email", "text": "Email" },
+ { "value": "phone", "text": "Phone" },
+ { "value": "mail", "text": "Physical Mail" }
+ ]
+ }
+ },
+ "mailingAddress": {
+ "type": "string",
+ "title": "Mailing Address",
+ "ui": {
+ "element": "textarea",
+ "class": "form-group",
+ "order": 4,
+ "rows": 3
+ }
+ }
+ },
+ "dependentRequired": {
+ "hasPhone": [ "phoneNumber" ],
+ "preferredContact": [ "preferredContact" ]
+ },
+ "if": {
+ "properties": {
+ "preferredContact": { "const": "phone" }
+ }
+ },
+ "then": {
+ "required": [ "phoneNumber" ]
+ },
+ "else": {
+ "if": {
+ "properties": {
+ "preferredContact": { "const": "mail" }
+ }
+ },
+ "then": {
+ "required": [ "mailingAddress" ]
+ }
+ }
+ },
+ "compositionValidations": {
+ "type": "object",
+ "title": "Composition Validation Examples (allOf, anyOf, oneOf)",
+ "properties": {
+ "contactMethod": {
+ "title": "Contact Method Validation",
+ "anyOf": [
+ {
+ "type": "object",
+ "properties": {
+ "email": {
+ "type": "string",
+ "format": "email",
+ "title": "Email Address"
+ }
+ },
+ "required": [ "email" ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "phone": {
+ "type": "string",
+ "pattern": "^\\+?[1-9]\\d{1,14}$",
+ "title": "Phone Number"
+ }
+ },
+ "required": [ "phone" ]
+ }
+ ],
+ "ui": {
+ "element": "fieldset",
+ "class": "contact-methods",
+ "order": 1
+ }
+ },
+ "paymentMethod": {
+ "title": "Payment Method (One Of)",
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "type": { "const": "credit_card" },
+ "cardNumber": {
+ "type": "string",
+ "pattern": "^\\d{16}$",
+ "title": "Card Number"
+ },
+ "expiryDate": {
+ "type": "string",
+ "pattern": "^(0[1-9]|1[0-2])\\/\\d{2}$",
+ "title": "Expiry Date (MM/YY)"
+ }
+ },
+ "required": [ "type", "cardNumber", "expiryDate" ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": { "const": "paypal" },
+ "paypalEmail": {
+ "type": "string",
+ "format": "email",
+ "title": "PayPal Email"
+ }
+ },
+ "required": [ "type", "paypalEmail" ]
+ }
+ ],
+ "ui": {
+ "element": "fieldset",
+ "class": "payment-methods",
+ "order": 2
+ }
+ }
+ }
+ },
+ "advancedFeatures": {
+ "type": "object",
+ "title": "Advanced Features",
+ "properties": {
+ "constField": {
+ "const": "REQUIRED_VALUE",
+ "title": "Constant Value Field",
+ "ui": {
+ "element": "input",
+ "type": "hidden",
+ "value": "REQUIRED_VALUE"
+ }
+ },
+ "booleanField": {
+ "type": "boolean",
+ "title": "Boolean Checkbox",
+ "ui": {
+ "element": "input",
+ "type": "checkbox",
+ "class": "form-group"
+ }
+ },
+ "readOnlyField": {
+ "type": "string",
+ "title": "Read Only Field",
+ "default": "This is read only",
+ "readOnly": true,
+ "ui": {
+ "element": "input",
+ "type": "text",
+ "class": "form-group",
+ "readonly": true
+ }
+ },
+ "textareaField": {
+ "type": "string",
+ "title": "Large Text Area",
+ "maxLength": 500,
+ "ui": {
+ "element": "textarea",
+ "class": "form-group",
+ "rows": 4,
+ "placeholder": "Enter detailed information here..."
+ }
+ }
+ }
+ }
+ },
+ "required": [ "stringValidations" ],
+ "form": {
+ "class": "validation-demo-form",
+ "action": "/api/validate-form",
+ "method": "POST",
+ "groups": [
+ {
+ "title": {
+ "text": "String Validations",
+ "class": "section-header"
+ },
+ "class": "string-validation-section",
+ "fields": [
+ "stringValidations.basicString",
+ "stringValidations.patternString",
+ "stringValidations.emailField",
+ "stringValidations.urlField",
+ "stringValidations.dateField",
+ "stringValidations.enumField"
+ ]
+ },
+ {
+ "title": {
+ "text": "Numeric Validations",
+ "class": "section-header"
+ },
+ "class": "numeric-validation-section",
+ "fields": [
+ "numericValidations.integerField",
+ "numericValidations.numberField",
+ "numericValidations.rangeField"
+ ]
+ },
+ {
+ "title": {
+ "text": "Array Validations",
+ "class": "section-header"
+ },
+ "class": "array-validation-section",
+ "fields": [
+ "arrayValidations.multiSelect",
+ "arrayValidations.dynamicList"
+ ]
+ },
+ {
+ "title": {
+ "text": "Conditional Validations",
+ "class": "section-header"
+ },
+ "class": "conditional-validation-section",
+ "fields": [
+ "conditionalValidations.userType",
+ "conditionalValidations.individualInfo",
+ "conditionalValidations.businessInfo"
+ ]
+ },
+ {
+ "title": {
+ "text": "Dependent Fields",
+ "class": "section-header"
+ },
+ "class": "dependent-validation-section",
+ "fields": [
+ "dependentFields.hasPhone",
+ "dependentFields.phoneNumber",
+ "dependentFields.preferredContact",
+ "dependentFields.mailingAddress"
+ ]
+ },
+ {
+ "title": {
+ "text": "Composition Validations",
+ "class": "section-header"
+ },
+ "class": "composition-validation-section",
+ "fields": [
+ "compositionValidations.contactMethod",
+ "compositionValidations.paymentMethod"
+ ]
+ },
+ {
+ "title": {
+ "text": "Advanced Features",
+ "class": "section-header"
+ },
+ "class": "advanced-features-section",
+ "fields": [
+ "advancedFeatures.constField",
+ "advancedFeatures.booleanField",
+ "advancedFeatures.readOnlyField",
+ "advancedFeatures.textareaField"
+ ]
+ }
+ ],
+ "submit": {
+ "label": "Validate Form",
+ "class": "btn btn-primary"
+ },
+ "reset": {
+ "label": "Reset All",
+ "class": "btn btn-secondary"
+ }
+ }
+}
diff --git a/examples/validation-demo/README.md b/examples/validation-demo/README.md
new file mode 100644
index 0000000..40ad82f
--- /dev/null
+++ b/examples/validation-demo/README.md
@@ -0,0 +1,103 @@
+# JSON Schema Validation Examples
+
+This directory contains comprehensive examples demonstrating all the validation features implemented in the JSON Schema form builder.
+
+## Files
+
+### Schema Examples
+
+1. **`comprehensive-validation.json`** - Complete example showcasing all JSON Schema 2020-12 validation features:
+ - Personal information with string validations (minLength, maxLength, pattern, format)
+ - Address with conditional validations based on country
+ - Preferences with array validations and enum constraints
+ - Financial information with numeric validations and conditional requirements
+ - Skills array with object validation
+ - Portfolio with anyOf compositions
+ - Terms acceptance with const validation
+
+2. **`validation-features.json`** - Organized examples by validation category:
+ - **String Validations**: Basic length limits, pattern matching, format validation (email, URL, date), enum selection
+ - **Numeric Validations**: Integer ranges, number with exclusive bounds, multipleOf, range sliders
+ - **Array Validations**: Multi-select with constraints, dynamic object arrays
+ - **Conditional Validations**: if/then/else logic based on user type selection
+ - **Dependent Fields**: dependentRequired, conditional field requirements
+ - **Composition Validations**: anyOf for flexible contact methods, oneOf for exclusive payment methods
+ - **Advanced Features**: const fields, boolean checkboxes, read-only fields, textarea with maxLength
+
+3. **`complex.json`** - Original company registration form with nested objects and grouped fields
+
+### Demo Server
+
+**`main.go`** - HTTP server that renders the schema examples into interactive HTML forms with:
+- Real-time client-side validation
+- HTML5 form controls with validation attributes
+- Tailwind CSS styling
+- JavaScript validation feedback
+- Form submission handling
+
+## Validation Features Demonstrated
+
+### String Validations
+- `minLength` / `maxLength` - Length constraints
+- `pattern` - Regular expression validation
+- `format` - Built-in formats (email, uri, date, etc.)
+- `enum` - Predefined value lists
+
+### Numeric Validations
+- `minimum` / `maximum` - Inclusive bounds
+- `exclusiveMinimum` / `exclusiveMaximum` - Exclusive bounds
+- `multipleOf` - Value must be multiple of specified number
+- Integer vs Number types
+
+### Array Validations
+- `minItems` / `maxItems` - Size constraints
+- `uniqueItems` - No duplicate values
+- `items` - Schema for array elements
+- Multi-select form controls
+
+### Object Validations
+- `required` - Required properties
+- `dependentRequired` - Fields required based on other fields
+- `dependentSchemas` - Schema changes based on other fields
+- Nested object structures
+
+### Conditional Logic
+- `if` / `then` / `else` - Conditional schema application
+- `allOf` - Must satisfy all sub-schemas
+- `anyOf` - Must satisfy at least one sub-schema
+- `oneOf` - Must satisfy exactly one sub-schema
+
+### Advanced Features
+- `const` - Constant values
+- `default` - Default values
+- `readOnly` - Read-only fields
+- Boolean validation
+- HTML form grouping and styling
+
+## Running the Demo
+
+1. Navigate to the validation-demo directory:
+ ```bash
+ cd examples/validation-demo
+ ```
+
+2. Run the server:
+ ```bash
+ go run main.go
+ ```
+
+3. Open your browser to `http://localhost:8080`
+
+4. Explore the different validation examples:
+ - Comprehensive Validation Demo - Complete real-world form
+ - Validation Features Demo - Organized by validation type
+ - Complex Form Demo - Original nested object example
+
+## Implementation Details
+
+The validation system is implemented in:
+- `/renderer/validation.go` - Core validation logic and HTML attribute generation
+- `/renderer/schema.go` - Form rendering with validation integration
+- Client-side JavaScript - Real-time validation feedback
+
+All examples demonstrate both server-side schema validation and client-side HTML5 validation attributes working together.
diff --git a/examples/validation-demo/main.go b/examples/validation-demo/main.go
new file mode 100644
index 0000000..e5dea08
--- /dev/null
+++ b/examples/validation-demo/main.go
@@ -0,0 +1,173 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "log"
+ "net/http"
+ "os"
+
+ "github.com/oarkflow/jsonschema"
+ "github.com/oarkflow/mq/renderer"
+)
+
+func main() {
+ // Setup routes
+ http.HandleFunc("/", indexHandler)
+ http.HandleFunc("/comprehensive", func(w http.ResponseWriter, r *http.Request) {
+ renderFormHandler(w, r, "comprehensive-validation.json", "Comprehensive Validation Demo")
+ })
+ http.HandleFunc("/features", func(w http.ResponseWriter, r *http.Request) {
+ renderFormHandler(w, r, "validation-features.json", "Validation Features Demo")
+ })
+ http.HandleFunc("/complex", func(w http.ResponseWriter, r *http.Request) {
+ renderFormHandler(w, r, "complex.json", "Complex Form Demo")
+ })
+
+ fmt.Println("Server starting on :8080")
+ fmt.Println("Available examples:")
+ fmt.Println(" - http://localhost:8080/comprehensive - Comprehensive validation demo")
+ fmt.Println(" - http://localhost:8080/features - Validation features demo")
+ fmt.Println(" - http://localhost:8080/complex - Complex form demo")
+
+ log.Fatal(http.ListenAndServe(":8080", nil))
+}
+
+func renderFormHandler(w http.ResponseWriter, r *http.Request, schemaFile, title string) {
+ // Load and compile schema
+ schemaData, err := os.ReadFile(schemaFile)
+ if err != nil {
+ http.Error(w, fmt.Sprintf("Error reading schema file: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ // Parse JSON schema
+ var schemaMap map[string]interface{}
+ if err := json.Unmarshal(schemaData, &schemaMap); err != nil {
+ http.Error(w, fmt.Sprintf("Error parsing JSON schema: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ // Compile schema
+ compiler := jsonschema.NewCompiler()
+ schema, err := compiler.Compile(schemaData)
+ if err != nil {
+ http.Error(w, fmt.Sprintf("Error compiling schema: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ // Create renderer with basic template
+ basicTemplate := `
+
+ `
+
+ renderer := renderer.NewJSONSchemaRenderer(schema, basicTemplate)
+
+ // Render form with empty data
+ html, err := renderer.RenderFields(map[string]any{})
+ if err != nil {
+ http.Error(w, fmt.Sprintf("Error rendering form: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ // Create complete HTML page
+ fullHTML := createFullHTMLPage(title, html)
+ w.Header().Set("Content-Type", "text/html")
+ w.Write([]byte(fullHTML))
+}
+
+func indexHandler(w http.ResponseWriter, r *http.Request) {
+ html := "" +
+ "" +
+ "" +
+ "JSON Schema Form Builder - Validation Examples" +
+ "" +
+ "" +
+ "" +
+ "JSON Schema Form Builder - Validation Examples
" +
+ "This demo showcases comprehensive JSON Schema 2020-12 validation support with:
" +
+ "" +
+ "- String validations (minLength, maxLength, pattern, format)
" +
+ "- Numeric validations (minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf)
" +
+ "- Array validations (minItems, maxItems, uniqueItems)
" +
+ "- Conditional validations (if/then/else, allOf, anyOf, oneOf)
" +
+ "- Dependent validations (dependentRequired, dependentSchemas)
" +
+ "- Client-side and server-side validation
" +
+ "- Advanced HTML form generation with validation attributes
" +
+ "
" +
+ "Available Examples:
" +
+ "" +
+ "- " +
+ "Comprehensive Validation Demo" +
+ "
Complete example with personal info, address, preferences, financial data, skills, portfolio, and complex conditional logic
" +
+ " " +
+ "- " +
+ "Validation Features Demo" +
+ "
Focused examples of specific validation features organized by category
" +
+ " " +
+ "- " +
+ "Complex Form Demo" +
+ "
Original company registration form with nested objects and grouped fields
" +
+ " " +
+ "
" +
+ "" +
+ ""
+
+ w.Header().Set("Content-Type", "text/html")
+ w.Write([]byte(html))
+}
+
+func createFullHTMLPage(title, formHTML string) string {
+ template := "" +
+ "" +
+ "" +
+ "" + title + "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "
← Back to Examples" +
+ "
" +
+ "
" + title + "
" +
+ "
This form demonstrates comprehensive JSON Schema validation.
" +
+ formHTML +
+ "
" +
+ "" +
+ ""
+
+ return template
+}
diff --git a/renderer/schema.go b/renderer/schema.go
index 914a024..62930c2 100644
--- a/renderer/schema.go
+++ b/renderer/schema.go
@@ -139,6 +139,32 @@ var voidElements = map[string]bool{
"param": true, "source": true, "track": true, "wbr": true,
}
+// Template cache for compiled templates
+var (
+ compiledFieldTemplates = make(map[string]*template.Template)
+ compiledGroupTemplate *template.Template
+ templateCacheMutex sync.RWMutex
+)
+
+// Initialize compiled templates once
+func init() {
+ var err error
+ compiledGroupTemplate, err = template.New("group").Parse(groupTemplateStr)
+ if err != nil {
+ panic(fmt.Sprintf("Failed to compile group template: %v", err))
+ }
+
+ templateCacheMutex.Lock()
+ defer templateCacheMutex.Unlock()
+
+ for element, tmplStr := range fieldTemplates {
+ compiled, err := template.New(element).Parse(tmplStr)
+ if err == nil {
+ compiledFieldTemplates[element] = compiled
+ }
+ }
+}
+
var standardAttrs = []string{
"id", "class", "name", "type", "value", "placeholder", "href", "src",
"alt", "title", "target", "rel", "role", "tabindex", "accesskey",
@@ -160,6 +186,7 @@ type FieldInfo struct {
Order int
Schema *jsonschema.Schema
IsRequired bool
+ Validation ValidationInfo // Add validation information
}
// GroupInfo represents metadata for a group extracted from JSONSchema
@@ -177,17 +204,79 @@ type GroupTitle struct {
// JSONSchemaRenderer is responsible for rendering HTML fields based on JSONSchema
type JSONSchemaRenderer struct {
- Schema *jsonschema.Schema
- HTMLLayout string
- cachedHTML string // Cached rendered HTML
+ Schema *jsonschema.Schema
+ HTMLLayout string
+ compiledLayout *template.Template
+ cachedGroups []GroupInfo
+ cachedButtons string
+ formConfig FormConfig
+ cacheMutex sync.RWMutex
+}
+
+// FormConfig holds cached form configuration
+type FormConfig struct {
+ Class string
+ Action string
+ Method string
+ Enctype string
}
// NewJSONSchemaRenderer creates a new instance of JSONSchemaRenderer
func NewJSONSchemaRenderer(schema *jsonschema.Schema, htmlLayout string) *JSONSchemaRenderer {
- return &JSONSchemaRenderer{
+ renderer := &JSONSchemaRenderer{
Schema: schema,
HTMLLayout: htmlLayout,
}
+
+ // Pre-compile layout template
+ renderer.compileLayoutTemplate()
+
+ // Pre-parse and cache groups and form config
+ renderer.precomputeStaticData()
+
+ return renderer
+}
+
+// compileLayoutTemplate pre-compiles the layout template
+func (r *JSONSchemaRenderer) compileLayoutTemplate() {
+ tmpl, err := template.New("layout").Funcs(template.FuncMap{
+ "form_groups": func(groupsHTML string) template.HTML {
+ return template.HTML(groupsHTML)
+ },
+ "form_buttons": func() template.HTML {
+ return template.HTML(r.cachedButtons)
+ },
+ "form_attributes": func(formAction string) template.HTMLAttr {
+ return template.HTMLAttr(fmt.Sprintf(`class="%s" action="%s" method="%s" enctype="%s"`,
+ r.formConfig.Class, formAction, r.formConfig.Method, r.formConfig.Enctype))
+ },
+ }).Parse(r.HTMLLayout)
+
+ if err == nil {
+ r.compiledLayout = tmpl
+ }
+}
+
+// precomputeStaticData caches groups and form configuration
+func (r *JSONSchemaRenderer) precomputeStaticData() {
+ r.cachedGroups = r.parseGroupsFromSchema()
+ r.cachedButtons = r.renderButtons()
+
+ // Cache form configuration
+ if r.Schema.Form != nil {
+ if class, ok := r.Schema.Form["class"].(string); ok {
+ r.formConfig.Class = class
+ }
+ if action, ok := r.Schema.Form["action"].(string); ok {
+ r.formConfig.Action = action
+ }
+ if method, ok := r.Schema.Form["method"].(string); ok {
+ r.formConfig.Method = method
+ }
+ if enctype, ok := r.Schema.Form["enctype"].(string); ok {
+ r.formConfig.Enctype = enctype
+ }
+ }
}
// interpolateTemplate replaces template placeholders with actual values
@@ -220,35 +309,65 @@ func (r *JSONSchemaRenderer) interpolateTemplate(templateStr string, data map[st
// RenderFields generates HTML for fields based on the JSONSchema
func (r *JSONSchemaRenderer) RenderFields(data map[string]any) (string, error) {
- // Return cached HTML if available
- if r.cachedHTML != "" {
- return r.cachedHTML, nil
+ r.cacheMutex.RLock()
+ defer r.cacheMutex.RUnlock()
+
+ // Use a string builder for efficient string concatenation
+ var groupHTML strings.Builder
+ groupHTML.Grow(1024) // Pre-allocate reasonable capacity
+
+ for _, group := range r.cachedGroups {
+ groupHTML.WriteString(renderGroupOptimized(group))
}
- groups := r.parseGroupsFromSchema()
- var groupHTML bytes.Buffer
- for _, group := range groups {
- groupHTML.WriteString(renderGroup(group))
+ // Interpolate dynamic form action
+ formAction := r.interpolateTemplate(r.formConfig.Action, data)
+
+ // Use pre-compiled template if available
+ if r.compiledLayout != nil {
+ var renderedHTML bytes.Buffer
+
+ templateData := struct {
+ GroupsHTML string
+ FormAction string
+ }{
+ GroupsHTML: groupHTML.String(),
+ FormAction: formAction,
+ }
+
+ // Update the template functions with current data
+ updatedTemplate := r.compiledLayout.Funcs(template.FuncMap{
+ "form_groups": func() template.HTML {
+ return template.HTML(templateData.GroupsHTML)
+ },
+ "form_buttons": func() template.HTML {
+ return template.HTML(r.cachedButtons)
+ },
+ "form_attributes": func() template.HTMLAttr {
+ return template.HTMLAttr(fmt.Sprintf(`class="%s" action="%s" method="%s" enctype="%s"`,
+ r.formConfig.Class, templateData.FormAction, r.formConfig.Method, r.formConfig.Enctype))
+ },
+ })
+
+ if err := updatedTemplate.Execute(&renderedHTML, nil); err != nil {
+ return "", fmt.Errorf("failed to execute compiled template: %w", err)
+ }
+
+ return renderedHTML.String(), nil
}
- // Extract form configuration
- var formClass, formAction, formMethod, formEnctype string
- if r.Schema.Form != nil {
- if class, ok := r.Schema.Form["class"].(string); ok {
- formClass = class
- }
- if action, ok := r.Schema.Form["action"].(string); ok {
- formAction = r.interpolateTemplate(action, data)
- }
- if method, ok := r.Schema.Form["method"].(string); ok {
- formMethod = method
- }
- if enctype, ok := r.Schema.Form["enctype"].(string); ok {
- formEnctype = enctype
- }
+ // Fallback to original method if compilation failed
+ return r.renderFieldsFallback(data)
+}
+
+// renderFieldsFallback provides fallback rendering when template compilation fails
+func (r *JSONSchemaRenderer) renderFieldsFallback(data map[string]any) (string, error) {
+ var groupHTML strings.Builder
+ for _, group := range r.cachedGroups {
+ groupHTML.WriteString(renderGroupOptimized(group))
}
- buttonsHTML := r.renderButtons()
+ formAction := r.interpolateTemplate(r.formConfig.Action, data)
// Create a new template with the layout and functions
tmpl, err := template.New("layout").Funcs(template.FuncMap{
@@ -256,11 +375,11 @@ func (r *JSONSchemaRenderer) RenderFields(data map[string]any) (string, error) {
return template.HTML(groupHTML.String())
},
"form_buttons": func() template.HTML {
- return template.HTML(buttonsHTML)
+ return template.HTML(r.cachedButtons)
},
"form_attributes": func() template.HTMLAttr {
return template.HTMLAttr(fmt.Sprintf(`class="%s" action="%s" method="%s" enctype="%s"`,
- formClass, formAction, formMethod, formEnctype))
+ r.formConfig.Class, formAction, r.formConfig.Method, r.formConfig.Enctype))
},
}).Parse(r.HTMLLayout)
if err != nil {
@@ -272,8 +391,7 @@ func (r *JSONSchemaRenderer) RenderFields(data map[string]any) (string, error) {
return "", fmt.Errorf("failed to execute HTML template: %w", err)
}
- r.cachedHTML = renderedHTML.String()
- return r.cachedHTML, nil
+ return renderedHTML.String(), nil
}
// parseGroupsFromSchema extracts and sorts groups and fields from schema
@@ -386,6 +504,7 @@ func (r *JSONSchemaRenderer) extractFieldsFromPath(fieldPath, parentPath string)
Order: order,
Schema: schema,
IsRequired: isRequired,
+ Validation: extractValidationInfo(schema, isRequired),
})
}
@@ -423,6 +542,7 @@ func (r *JSONSchemaRenderer) extractFieldsFromNestedSchema(propName, parentPath
Order: order,
Schema: propSchema,
IsRequired: isRequired,
+ Validation: extractValidationInfo(propSchema, isRequired),
})
}
@@ -481,18 +601,58 @@ func renderGroup(group GroupInfo) string {
return groupHTML.String()
}
+// renderGroupOptimized uses pre-compiled templates and string builder for better performance
+func renderGroupOptimized(group GroupInfo) string {
+ // Collect all validations for dependency checking
+ allValidations := make(map[string]ValidationInfo)
+ for _, field := range group.Fields {
+ allValidations[field.FieldPath] = field.Validation
+ }
+
+ // Use string builder for better performance
+ var fieldsHTML strings.Builder
+ fieldsHTML.Grow(512) // Pre-allocate reasonable capacity
+
+ for _, field := range group.Fields {
+ fieldsHTML.WriteString(renderFieldWithContext(field, allValidations))
+ }
+
+ // Use pre-compiled group template
+ templateCacheMutex.RLock()
+ groupTemplate := compiledGroupTemplate
+ templateCacheMutex.RUnlock()
+
+ if groupTemplate != nil {
+ var groupHTML bytes.Buffer
+ data := map[string]any{
+ "Title": group.Title,
+ "GroupClass": group.GroupClass,
+ "FieldsHTML": template.HTML(fieldsHTML.String()),
+ }
+
+ if err := groupTemplate.Execute(&groupHTML, data); err == nil {
+ return groupHTML.String()
+ }
+ }
+
+ // Fallback to original method
+ return renderGroup(group)
+}
+
func renderField(field FieldInfo) string {
- if field.Schema.UI == nil {
+ return renderFieldWithContext(field, make(map[string]ValidationInfo))
+}
+
+// renderFieldWithContext renders a field with access to all field validations for dependency checking
+func renderFieldWithContext(field FieldInfo, allValidations map[string]ValidationInfo) string {
+ // Determine the appropriate element type based on schema
+ element := determineFieldElement(field.Schema)
+ if element == "" {
return ""
}
- element, ok := field.Schema.UI["element"].(string)
- if !ok || element == "" {
- return ""
- }
-
- // Build all attributes
- allAttributes := buildAllAttributes(field)
+ // Build all attributes including validation
+ allAttributes := buildAllAttributesWithValidation(field)
// Get content
content := getFieldContent(field)
@@ -501,14 +661,20 @@ func renderField(field FieldInfo) string {
// Generate label if needed
labelHTML := generateLabel(field)
+ // Generate options for select/radio elements
+ optionsHTML := generateOptionsFromSchema(field.Schema)
+
+ // Generate client-side validation with context
+ validationJS := generateClientSideValidation(field.FieldPath, field.Validation, allValidations)
+
data := map[string]any{
"Element": element,
"AllAttributes": template.HTMLAttr(allAttributes),
"Content": content,
- "ContentHTML": template.HTML(contentHTML),
+ "ContentHTML": template.HTML(contentHTML + validationJS),
"LabelHTML": template.HTML(labelHTML),
- "Class": getUIValue(field.Schema.UI, "class"),
- "OptionsHTML": template.HTML(generateOptions(field.Schema.UI)),
+ "Class": getFieldWrapperClass(field.Schema),
+ "OptionsHTML": template.HTML(optionsHTML),
}
// Use specific template if available, otherwise use generic
@@ -529,6 +695,212 @@ func renderField(field FieldInfo) string {
return buf.String()
}
+// determineFieldElement determines the appropriate HTML element based on JSON Schema
+func determineFieldElement(schema *jsonschema.Schema) string {
+ // Check UI element first
+ if schema.UI != nil {
+ if element, ok := schema.UI["element"].(string); ok {
+ return element
+ }
+ }
+
+ // Auto-determine based on schema properties
+ if shouldUseSelect(schema) {
+ return "select"
+ }
+
+ if shouldUseTextarea(schema) {
+ return "textarea"
+ }
+
+ // Default to input for most cases
+ var typeStr string
+ if len(schema.Type) > 0 {
+ typeStr = schema.Type[0]
+ }
+
+ switch typeStr {
+ case "boolean":
+ return "input" // will be type="checkbox"
+ case "array":
+ return "input" // could be enhanced for array inputs
+ case "object":
+ return "fieldset" // for nested objects
+ default:
+ return "input"
+ }
+}
+
+// buildAllAttributesWithValidation creates all HTML attributes including validation
+func buildAllAttributesWithValidation(field FieldInfo) string {
+ var builder strings.Builder
+ builder.Grow(512) // Pre-allocate capacity
+
+ // Use the field path as the name attribute for nested fields
+ fieldName := field.FieldPath
+ if fieldName == "" {
+ fieldName = field.Name
+ }
+
+ // Add name attribute
+ builder.WriteString(`name="`)
+ builder.WriteString(fieldName)
+ builder.WriteString(`"`)
+
+ // Add id attribute for accessibility
+ fieldId := strings.ReplaceAll(fieldName, ".", "_")
+ builder.WriteString(` id="`)
+ builder.WriteString(fieldId)
+ builder.WriteString(`"`)
+
+ // Determine and add type attribute for input elements
+ element := determineFieldElement(field.Schema)
+ if element == "input" {
+ inputType := getInputTypeFromSchema(field.Schema)
+ builder.WriteString(` type="`)
+ builder.WriteString(inputType)
+ builder.WriteString(`"`)
+ }
+
+ // Add validation attributes
+ validationAttrs := generateValidationAttributes(field.Validation)
+ for _, attr := range validationAttrs {
+ builder.WriteString(` `)
+ builder.WriteString(attr)
+ }
+
+ // Add default value
+ if defaultValue := getDefaultValue(field.Schema); defaultValue != "" {
+ builder.WriteString(` value="`)
+ builder.WriteString(defaultValue)
+ builder.WriteString(`"`)
+ }
+
+ // Add placeholder
+ if placeholder := getPlaceholder(field.Schema); placeholder != "" {
+ builder.WriteString(` placeholder="`)
+ builder.WriteString(placeholder)
+ builder.WriteString(`"`)
+ }
+
+ // Add standard attributes from UI
+ if field.Schema.UI != nil {
+ for _, attr := range standardAttrs {
+ if attr == "name" || attr == "id" || attr == "type" || attr == "required" ||
+ attr == "pattern" || attr == "min" || attr == "max" || attr == "minlength" ||
+ attr == "maxlength" || attr == "step" || attr == "value" || attr == "placeholder" {
+ continue // Already handled above or in validation
+ }
+ if value, exists := field.Schema.UI[attr]; exists {
+ if attr == "class" && value == "" {
+ continue // Skip empty class
+ }
+ builder.WriteString(` `)
+ builder.WriteString(attr)
+ builder.WriteString(`="`)
+ builder.WriteString(fmt.Sprintf("%v", value))
+ builder.WriteString(`"`)
+ }
+ }
+
+ // Add data-* and aria-* attributes
+ for key, value := range field.Schema.UI {
+ if strings.HasPrefix(key, "data-") || strings.HasPrefix(key, "aria-") {
+ builder.WriteString(` `)
+ builder.WriteString(key)
+ builder.WriteString(`="`)
+ builder.WriteString(fmt.Sprintf("%v", value))
+ builder.WriteString(`"`)
+ }
+ }
+ }
+
+ return builder.String()
+}
+
+// generateOptionsFromSchema generates option HTML from schema enum or UI options
+func generateOptionsFromSchema(schema *jsonschema.Schema) string {
+ var optionsHTML strings.Builder
+
+ // Check UI options first
+ if schema.UI != nil {
+ if options, ok := schema.UI["options"].([]interface{}); ok {
+ for _, option := range options {
+ if optionMap, ok := option.(map[string]interface{}); ok {
+ value := getMapValue(optionMap, "value", "")
+ text := getMapValue(optionMap, "text", value)
+ selected := ""
+ if isSelected, ok := optionMap["selected"].(bool); ok && isSelected {
+ selected = ` selected="selected"`
+ }
+ disabled := ""
+ if isDisabled, ok := optionMap["disabled"].(bool); ok && isDisabled {
+ disabled = ` disabled="disabled"`
+ }
+ optionsHTML.WriteString(fmt.Sprintf(``,
+ value, selected, disabled, text))
+ } else {
+ optionsHTML.WriteString(fmt.Sprintf(``, option, option))
+ }
+ }
+ return optionsHTML.String()
+ }
+ }
+
+ // Generate options from enum
+ if len(schema.Enum) > 0 {
+ for _, enumValue := range schema.Enum {
+ optionsHTML.WriteString(fmt.Sprintf(``, enumValue, enumValue))
+ }
+ }
+
+ return optionsHTML.String()
+}
+
+// getFieldWrapperClass determines the CSS class for the field wrapper
+func getFieldWrapperClass(schema *jsonschema.Schema) string {
+ if schema.UI != nil {
+ if class, ok := schema.UI["class"].(string); ok {
+ return class
+ }
+ }
+ return "form-group" // default class
+}
+
+// getDefaultValue extracts default value from schema
+func getDefaultValue(schema *jsonschema.Schema) string {
+ if schema.Default != nil {
+ return fmt.Sprintf("%v", schema.Default)
+ }
+ return ""
+}
+
+// getPlaceholder extracts placeholder from schema
+func getPlaceholder(schema *jsonschema.Schema) string {
+ if schema.UI != nil {
+ if placeholder, ok := schema.UI["placeholder"].(string); ok {
+ return placeholder
+ }
+ }
+
+ // Auto-generate placeholder from title or description
+ if schema.Title != nil {
+ return fmt.Sprintf("Enter %s", strings.ToLower(*schema.Title))
+ }
+
+ if schema.Description != nil {
+ return *schema.Description
+ }
+
+ return ""
+}
+
+// renderFieldOptimized uses pre-compiled templates and optimized attribute building
+func renderFieldOptimized(field FieldInfo) string {
+ // Use the new comprehensive rendering logic
+ return renderField(field)
+}
+
func buildAllAttributes(field FieldInfo) string {
var attributes []string
@@ -593,6 +965,88 @@ func buildAllAttributes(field FieldInfo) string {
return strings.Join(attributes, " ")
}
+// buildAllAttributesOptimized uses string builder for better performance
+func buildAllAttributesOptimized(field FieldInfo) string {
+ var builder strings.Builder
+ builder.Grow(256) // Pre-allocate reasonable capacity
+
+ // Use the field path as the name attribute for nested fields
+ fieldName := field.FieldPath
+ if fieldName == "" {
+ fieldName = field.Name
+ }
+
+ // Add name attribute
+ builder.WriteString(`name="`)
+ builder.WriteString(fieldName)
+ builder.WriteString(`"`)
+
+ // Add standard attributes from UI
+ if field.Schema.UI != nil {
+ element, _ := field.Schema.UI["element"].(string)
+ for _, attr := range standardAttrs {
+ if attr == "name" {
+ continue // Already handled above
+ }
+ if value, exists := field.Schema.UI[attr]; exists {
+ if attr == "class" && value == "" {
+ continue // Skip empty class
+ }
+ // For select, input, textarea, add class to element itself
+ if attr == "class" && (element == "select" || element == "input" || element == "textarea") {
+ builder.WriteString(` class="`)
+ builder.WriteString(fmt.Sprintf("%v", value))
+ builder.WriteString(`"`)
+ continue
+ }
+ // For other elements, do not add class here (it will be handled in the wrapper div)
+ if attr == "class" {
+ continue
+ }
+ builder.WriteString(` `)
+ builder.WriteString(attr)
+ builder.WriteString(`="`)
+ builder.WriteString(fmt.Sprintf("%v", value))
+ builder.WriteString(`"`)
+ }
+ }
+
+ // Add data-* and aria-* attributes
+ for key, value := range field.Schema.UI {
+ if strings.HasPrefix(key, "data-") || strings.HasPrefix(key, "aria-") {
+ builder.WriteString(` `)
+ builder.WriteString(key)
+ builder.WriteString(`="`)
+ builder.WriteString(fmt.Sprintf("%v", value))
+ builder.WriteString(`"`)
+ }
+ }
+ }
+
+ // Handle required field
+ if field.IsRequired {
+ currentAttrs := builder.String()
+ if !strings.Contains(currentAttrs, "required=") {
+ builder.WriteString(` required="required"`)
+ }
+ }
+
+ // Handle input type based on field type
+ element, _ := field.Schema.UI["element"].(string)
+ if element == "input" {
+ if inputType := getInputType(field.Schema); inputType != "" {
+ currentAttrs := builder.String()
+ if !strings.Contains(currentAttrs, "type=") {
+ builder.WriteString(` type="`)
+ builder.WriteString(inputType)
+ builder.WriteString(`"`)
+ }
+ }
+ }
+
+ return builder.String()
+}
+
func getInputType(schema *jsonschema.Schema) string {
// Check UI type first
if schema.UI != nil {
@@ -835,14 +1289,42 @@ type RequestSchemaTemplate struct {
Renderer *JSONSchemaRenderer `json:"template"`
}
-var cache = make(map[string]*RequestSchemaTemplate)
-var mu = &sync.Mutex{}
-var BaseTemplateDir = "templates"
+var (
+ cache = make(map[string]*RequestSchemaTemplate)
+ mu = &sync.RWMutex{}
+ BaseTemplateDir = "templates"
+)
-func Get(schemaPath, template string) (*JSONSchemaRenderer, error) {
+// ClearCache clears the template cache
+func ClearCache() {
mu.Lock()
defer mu.Unlock()
+ cache = make(map[string]*RequestSchemaTemplate)
+}
+
+// GetCacheSize returns the current cache size
+func GetCacheSize() int {
+ mu.RLock()
+ defer mu.RUnlock()
+ return len(cache)
+}
+
+func Get(schemaPath, template string) (*JSONSchemaRenderer, error) {
path := fmt.Sprintf("%s:%s", schemaPath, template)
+
+ // Try to get from cache with read lock first
+ mu.RLock()
+ if cached, exists := cache[path]; exists {
+ mu.RUnlock()
+ return cached.Renderer, nil
+ }
+ mu.RUnlock()
+
+ // If not in cache, acquire write lock and create
+ mu.Lock()
+ defer mu.Unlock()
+
+ // Double-check after acquiring write lock
if cached, exists := cache[path]; exists {
return cached.Renderer, nil
}
@@ -873,3 +1355,10 @@ func Get(schemaPath, template string) (*JSONSchemaRenderer, error) {
return cachedTemplate.Renderer, nil
}
+
+// ClearRenderCache clears the rendered HTML cache for a specific renderer
+func (r *JSONSchemaRenderer) ClearRenderCache() {
+ r.cacheMutex.Lock()
+ defer r.cacheMutex.Unlock()
+ // Since we're not caching static HTML anymore, this is just for API consistency
+}
diff --git a/renderer/validation.go b/renderer/validation.go
new file mode 100644
index 0000000..04e3a8b
--- /dev/null
+++ b/renderer/validation.go
@@ -0,0 +1,663 @@
+package renderer
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/oarkflow/jsonschema"
+)
+
+// ValidationInfo holds comprehensive validation information for a field
+type ValidationInfo struct {
+ // Basic validations
+ Required bool
+ MinLength *float64
+ MaxLength *float64
+ Minimum *jsonschema.Rat
+ Maximum *jsonschema.Rat
+ Pattern string
+ Format string
+ Enum []interface{}
+ MultipleOf *jsonschema.Rat
+ ExclusiveMin *jsonschema.Rat
+ ExclusiveMax *jsonschema.Rat
+ MinItems *float64
+ MaxItems *float64
+ UniqueItems bool
+ MinProperties *float64
+ MaxProperties *float64
+ Const interface{}
+
+ // Advanced JSON Schema 2020-12 validations
+ AllOf []*jsonschema.Schema
+ AnyOf []*jsonschema.Schema
+ OneOf []*jsonschema.Schema
+ Not *jsonschema.Schema
+ If *jsonschema.Schema
+ Then *jsonschema.Schema
+ Else *jsonschema.Schema
+ DependentSchemas map[string]*jsonschema.Schema
+ DependentRequired map[string][]string
+ PrefixItems []*jsonschema.Schema
+ Items *jsonschema.Schema
+ Contains *jsonschema.Schema
+ MaxContains *float64
+ MinContains *float64
+ UnevaluatedItems *jsonschema.Schema
+ UnevaluatedProperties *jsonschema.Schema
+ PropertyNames *jsonschema.Schema
+ AdditionalProperties *jsonschema.Schema
+ PatternProperties *jsonschema.SchemaMap
+
+ // Content validations
+ ContentEncoding *string
+ ContentMediaType *string
+ ContentSchema *jsonschema.Schema
+
+ // Metadata
+ Title *string
+ Description *string
+ Default interface{}
+ Examples []interface{}
+ Deprecated *bool
+ ReadOnly *bool
+ WriteOnly *bool
+}
+
+// extractValidationInfo extracts comprehensive validation information from JSON Schema
+func extractValidationInfo(schema *jsonschema.Schema, isRequired bool) ValidationInfo {
+ validation := ValidationInfo{
+ Required: isRequired,
+ }
+
+ // Basic string validations
+ if schema.MinLength != nil {
+ validation.MinLength = schema.MinLength
+ }
+ if schema.MaxLength != nil {
+ validation.MaxLength = schema.MaxLength
+ }
+ if schema.Pattern != nil {
+ validation.Pattern = *schema.Pattern
+ }
+ if schema.Format != nil {
+ validation.Format = *schema.Format
+ }
+
+ // Numeric validations
+ if schema.Minimum != nil {
+ validation.Minimum = schema.Minimum
+ }
+ if schema.Maximum != nil {
+ validation.Maximum = schema.Maximum
+ }
+ if schema.ExclusiveMinimum != nil {
+ validation.ExclusiveMin = schema.ExclusiveMinimum
+ }
+ if schema.ExclusiveMaximum != nil {
+ validation.ExclusiveMax = schema.ExclusiveMaximum
+ }
+ if schema.MultipleOf != nil {
+ validation.MultipleOf = schema.MultipleOf
+ }
+
+ // Array validations
+ if schema.MinItems != nil {
+ validation.MinItems = schema.MinItems
+ }
+ if schema.MaxItems != nil {
+ validation.MaxItems = schema.MaxItems
+ }
+ if schema.UniqueItems != nil {
+ validation.UniqueItems = *schema.UniqueItems
+ }
+ if schema.MaxContains != nil {
+ validation.MaxContains = schema.MaxContains
+ }
+ if schema.MinContains != nil {
+ validation.MinContains = schema.MinContains
+ }
+
+ // Object validations
+ if schema.MinProperties != nil {
+ validation.MinProperties = schema.MinProperties
+ }
+ if schema.MaxProperties != nil {
+ validation.MaxProperties = schema.MaxProperties
+ }
+
+ // Enum and const values
+ if schema.Enum != nil {
+ validation.Enum = schema.Enum
+ }
+ if schema.Const != nil {
+ validation.Const = *schema.Const
+ }
+
+ // Advanced JSON Schema 2020-12 features
+ if schema.AllOf != nil {
+ validation.AllOf = schema.AllOf
+ }
+ if schema.AnyOf != nil {
+ validation.AnyOf = schema.AnyOf
+ }
+ if schema.OneOf != nil {
+ validation.OneOf = schema.OneOf
+ }
+ if schema.Not != nil {
+ validation.Not = schema.Not
+ }
+
+ // Conditional validations
+ if schema.If != nil {
+ validation.If = schema.If
+ }
+ if schema.Then != nil {
+ validation.Then = schema.Then
+ }
+ if schema.Else != nil {
+ validation.Else = schema.Else
+ }
+
+ // Dependent validations
+ if schema.DependentSchemas != nil {
+ validation.DependentSchemas = schema.DependentSchemas
+ }
+ if schema.DependentRequired != nil {
+ validation.DependentRequired = schema.DependentRequired
+ }
+
+ // Array item validations
+ if schema.PrefixItems != nil {
+ validation.PrefixItems = schema.PrefixItems
+ }
+ if schema.Items != nil {
+ validation.Items = schema.Items
+ }
+ if schema.Contains != nil {
+ validation.Contains = schema.Contains
+ }
+ if schema.UnevaluatedItems != nil {
+ validation.UnevaluatedItems = schema.UnevaluatedItems
+ }
+
+ // Property validations
+ if schema.PropertyNames != nil {
+ validation.PropertyNames = schema.PropertyNames
+ }
+ if schema.AdditionalProperties != nil {
+ validation.AdditionalProperties = schema.AdditionalProperties
+ }
+ if schema.PatternProperties != nil {
+ validation.PatternProperties = schema.PatternProperties
+ }
+ if schema.UnevaluatedProperties != nil {
+ validation.UnevaluatedProperties = schema.UnevaluatedProperties
+ }
+
+ // Content validations
+ if schema.ContentEncoding != nil {
+ validation.ContentEncoding = schema.ContentEncoding
+ }
+ if schema.ContentMediaType != nil {
+ validation.ContentMediaType = schema.ContentMediaType
+ }
+ if schema.ContentSchema != nil {
+ validation.ContentSchema = schema.ContentSchema
+ }
+
+ // Metadata
+ if schema.Title != nil {
+ validation.Title = schema.Title
+ }
+ if schema.Description != nil {
+ validation.Description = schema.Description
+ }
+ if schema.Default != nil {
+ validation.Default = schema.Default
+ }
+ if schema.Examples != nil {
+ validation.Examples = schema.Examples
+ }
+ if schema.Deprecated != nil {
+ validation.Deprecated = schema.Deprecated
+ }
+ if schema.ReadOnly != nil {
+ validation.ReadOnly = schema.ReadOnly
+ }
+ if schema.WriteOnly != nil {
+ validation.WriteOnly = schema.WriteOnly
+ }
+
+ return validation
+}
+
+// generateValidationAttributes creates comprehensive HTML validation attributes
+func generateValidationAttributes(validation ValidationInfo) []string {
+ var attrs []string
+
+ if validation.Required {
+ attrs = append(attrs, `required="required"`)
+ }
+
+ // String validations
+ if validation.MinLength != nil {
+ attrs = append(attrs, fmt.Sprintf(`minlength="%.0f"`, *validation.MinLength))
+ }
+ if validation.MaxLength != nil {
+ attrs = append(attrs, fmt.Sprintf(`maxlength="%.0f"`, *validation.MaxLength))
+ }
+ if validation.Pattern != "" {
+ attrs = append(attrs, fmt.Sprintf(`pattern="%s"`, validation.Pattern))
+ }
+
+ // Numeric validations
+ if validation.Minimum != nil {
+ minVal, _ := validation.Minimum.Float64()
+ attrs = append(attrs, fmt.Sprintf(`min="%g"`, minVal))
+ }
+ if validation.Maximum != nil {
+ maxVal, _ := validation.Maximum.Float64()
+ attrs = append(attrs, fmt.Sprintf(`max="%g"`, maxVal))
+ }
+ if validation.ExclusiveMin != nil {
+ exclusiveMinVal, _ := validation.ExclusiveMin.Float64()
+ attrs = append(attrs, fmt.Sprintf(`data-exclusive-min="%g"`, exclusiveMinVal))
+ }
+ if validation.ExclusiveMax != nil {
+ exclusiveMaxVal, _ := validation.ExclusiveMax.Float64()
+ attrs = append(attrs, fmt.Sprintf(`data-exclusive-max="%g"`, exclusiveMaxVal))
+ }
+ if validation.MultipleOf != nil {
+ stepVal, _ := validation.MultipleOf.Float64()
+ attrs = append(attrs, fmt.Sprintf(`step="%g"`, stepVal))
+ }
+
+ // Array validations (for multi-select and array inputs)
+ if validation.MinItems != nil {
+ attrs = append(attrs, fmt.Sprintf(`data-min-items="%.0f"`, *validation.MinItems))
+ }
+ if validation.MaxItems != nil {
+ attrs = append(attrs, fmt.Sprintf(`data-max-items="%.0f"`, *validation.MaxItems))
+ }
+ if validation.UniqueItems {
+ attrs = append(attrs, `data-unique-items="true"`)
+ }
+
+ // Metadata attributes
+ if validation.Title != nil {
+ attrs = append(attrs, fmt.Sprintf(`title="%s"`, *validation.Title))
+ }
+ if validation.Description != nil {
+ attrs = append(attrs, fmt.Sprintf(`data-description="%s"`, *validation.Description))
+ }
+ if validation.Default != nil {
+ attrs = append(attrs, fmt.Sprintf(`data-default="%v"`, validation.Default))
+ }
+ if validation.ReadOnly != nil && *validation.ReadOnly {
+ attrs = append(attrs, `readonly="readonly"`)
+ }
+ if validation.Deprecated != nil && *validation.Deprecated {
+ attrs = append(attrs, `data-deprecated="true"`)
+ }
+
+ // Complex validation indicators (for client-side handling)
+ if len(validation.AllOf) > 0 {
+ attrs = append(attrs, `data-has-allof="true"`)
+ }
+ if len(validation.AnyOf) > 0 {
+ attrs = append(attrs, `data-has-anyof="true"`)
+ }
+ if len(validation.OneOf) > 0 {
+ attrs = append(attrs, `data-has-oneof="true"`)
+ }
+ if validation.Not != nil {
+ attrs = append(attrs, `data-has-not="true"`)
+ }
+ if validation.If != nil {
+ attrs = append(attrs, `data-has-conditional="true"`)
+ }
+ if len(validation.DependentRequired) > 0 {
+ attrs = append(attrs, `data-has-dependent-required="true"`)
+ }
+
+ return attrs
+}
+
+// generateClientSideValidation creates comprehensive JavaScript validation functions
+func generateClientSideValidation(fieldPath string, validation ValidationInfo, allFields map[string]ValidationInfo) string {
+ if !hasValidation(validation) {
+ return ""
+ }
+
+ var validations []string
+ var dependentValidations []string
+
+ // Basic validations
+ if validation.Required {
+ validations = append(validations, `
+ if (!value || (typeof value === 'string' && value.trim() === '')) {
+ return 'This field is required';
+ }`)
+ }
+
+ // String validations
+ if validation.MinLength != nil {
+ validations = append(validations, fmt.Sprintf(`
+ if (value && value.length < %.0f) {
+ return 'Minimum length is %.0f characters';
+ }`, *validation.MinLength, *validation.MinLength))
+ }
+ if validation.MaxLength != nil {
+ validations = append(validations, fmt.Sprintf(`
+ if (value && value.length > %.0f) {
+ return 'Maximum length is %.0f characters';
+ }`, *validation.MaxLength, *validation.MaxLength))
+ }
+ if validation.Pattern != "" {
+ validations = append(validations, fmt.Sprintf(`
+ if (value && !/%s/.test(value)) {
+ return 'Invalid format';
+ }`, strings.ReplaceAll(validation.Pattern, `\`, `\\`)))
+ }
+
+ // Numeric validations
+ if validation.Minimum != nil {
+ minVal, _ := validation.Minimum.Float64()
+ validations = append(validations, fmt.Sprintf(`
+ if (value !== '' && parseFloat(value) < %g) {
+ return 'Minimum value is %g';
+ }`, minVal, minVal))
+ }
+ if validation.Maximum != nil {
+ maxVal, _ := validation.Maximum.Float64()
+ validations = append(validations, fmt.Sprintf(`
+ if (value !== '' && parseFloat(value) > %g) {
+ return 'Maximum value is %g';
+ }`, maxVal, maxVal))
+ }
+ if validation.ExclusiveMin != nil {
+ exclusiveMinVal, _ := validation.ExclusiveMin.Float64()
+ validations = append(validations, fmt.Sprintf(`
+ if (value !== '' && parseFloat(value) <= %g) {
+ return 'Value must be greater than %g';
+ }`, exclusiveMinVal, exclusiveMinVal))
+ }
+ if validation.ExclusiveMax != nil {
+ exclusiveMaxVal, _ := validation.ExclusiveMax.Float64()
+ validations = append(validations, fmt.Sprintf(`
+ if (value !== '' && parseFloat(value) >= %g) {
+ return 'Value must be less than %g';
+ }`, exclusiveMaxVal, exclusiveMaxVal))
+ }
+
+ // Enum validations
+ if len(validation.Enum) > 0 {
+ enumValues := make([]string, len(validation.Enum))
+ for i, v := range validation.Enum {
+ enumValues[i] = fmt.Sprintf("'%v'", v)
+ }
+ validations = append(validations, fmt.Sprintf(`
+ var allowedValues = [%s];
+ if (value && allowedValues.indexOf(value) === -1) {
+ return "Value must be one of: %s";
+ }`, strings.Join(enumValues, ", "), strings.Join(enumValues, ", ")))
+ }
+
+ // Const validation
+ if validation.Const != nil {
+ validations = append(validations, fmt.Sprintf(`
+ if (value !== '%v') {
+ return 'Value must be %v';
+ }`, validation.Const, validation.Const))
+ }
+
+ // Array validations
+ if validation.MinItems != nil {
+ validations = append(validations, fmt.Sprintf(`
+ if (Array.isArray(value) && value.length < %.0f) {
+ return 'Minimum %.0f items required';
+ }`, *validation.MinItems, *validation.MinItems))
+ }
+ if validation.MaxItems != nil {
+ validations = append(validations, fmt.Sprintf(`
+ if (Array.isArray(value) && value.length > %.0f) {
+ return 'Maximum %.0f items allowed';
+ }`, *validation.MaxItems, *validation.MaxItems))
+ }
+ if validation.UniqueItems {
+ validations = append(validations, `
+ if (Array.isArray(value)) {
+ var unique = [...new Set(value)];
+ if (unique.length !== value.length) {
+ return 'All items must be unique';
+ }
+ }`)
+ }
+
+ // Dependent required validations
+ if len(validation.DependentRequired) > 0 {
+ for depField, requiredFields := range validation.DependentRequired {
+ for _, reqField := range requiredFields {
+ dependentValidations = append(dependentValidations, fmt.Sprintf(`
+ // Check if %s has value, then %s is required
+ var depField = document.querySelector('[name="%s"]');
+ var reqField = document.querySelector('[name="%s"]');
+ if (depField && depField.value && (!reqField || !reqField.value)) {
+ return 'Field %s is required when %s has a value';
+ }`, depField, reqField, reqField, depField))
+ }
+ }
+ }
+
+ // Complex validations warning (these need server-side validation)
+ var complexValidationWarnings []string
+ if len(validation.AllOf) > 0 {
+ complexValidationWarnings = append(complexValidationWarnings, "// AllOf validation requires server-side validation")
+ }
+ if len(validation.AnyOf) > 0 {
+ complexValidationWarnings = append(complexValidationWarnings, "// AnyOf validation requires server-side validation")
+ }
+ if len(validation.OneOf) > 0 {
+ complexValidationWarnings = append(complexValidationWarnings, "// OneOf validation requires server-side validation")
+ }
+ if validation.Not != nil {
+ complexValidationWarnings = append(complexValidationWarnings, "// Not validation requires server-side validation")
+ }
+ if validation.If != nil {
+ complexValidationWarnings = append(complexValidationWarnings, "// Conditional validation requires server-side validation")
+ }
+
+ if len(validations) == 0 && len(dependentValidations) == 0 {
+ return ""
+ }
+
+ fieldId := strings.ReplaceAll(fieldPath, ".", "_")
+ allValidations := append(validations, dependentValidations...)
+ if len(complexValidationWarnings) > 0 {
+ allValidations = append([]string{strings.Join(complexValidationWarnings, "\n\t\t\t")}, allValidations...)
+ }
+
+ return fmt.Sprintf(`
+
+ `, fieldPath, fieldId, strings.Join(allValidations, "\n"),
+ strings.Join(dependentValidations, "\n"), fieldId, fieldId, fieldId, fieldId, fieldId)
+}
+
+// hasValidation checks if validation info contains any validation rules
+func hasValidation(validation ValidationInfo) bool {
+ return validation.Required ||
+ validation.MinLength != nil ||
+ validation.MaxLength != nil ||
+ validation.Minimum != nil ||
+ validation.Maximum != nil ||
+ validation.Pattern != "" ||
+ validation.ExclusiveMin != nil ||
+ validation.ExclusiveMax != nil ||
+ validation.MultipleOf != nil ||
+ len(validation.Enum) > 0 ||
+ validation.Const != nil ||
+ validation.MinItems != nil ||
+ validation.MaxItems != nil ||
+ validation.UniqueItems ||
+ validation.MinProperties != nil ||
+ validation.MaxProperties != nil ||
+ len(validation.AllOf) > 0 ||
+ len(validation.AnyOf) > 0 ||
+ len(validation.OneOf) > 0 ||
+ validation.Not != nil ||
+ validation.If != nil ||
+ validation.Then != nil ||
+ validation.Else != nil ||
+ len(validation.DependentSchemas) > 0 ||
+ len(validation.DependentRequired) > 0 ||
+ len(validation.PrefixItems) > 0 ||
+ validation.Items != nil ||
+ validation.Contains != nil ||
+ validation.MaxContains != nil ||
+ validation.MinContains != nil ||
+ validation.UnevaluatedItems != nil ||
+ validation.UnevaluatedProperties != nil ||
+ validation.PropertyNames != nil ||
+ validation.AdditionalProperties != nil ||
+ validation.PatternProperties != nil ||
+ validation.ContentEncoding != nil ||
+ validation.ContentMediaType != nil ||
+ validation.ContentSchema != nil ||
+ validation.ReadOnly != nil ||
+ validation.WriteOnly != nil ||
+ validation.Deprecated != nil
+}
+
+// getInputTypeFromSchema determines the appropriate HTML input type based on JSON Schema
+func getInputTypeFromSchema(schema *jsonschema.Schema) string {
+ // Check UI type first
+ if schema.UI != nil {
+ if uiType, ok := schema.UI["type"].(string); ok {
+ return uiType
+ }
+ }
+
+ // Check format
+ if schema.Format != nil {
+ switch *schema.Format {
+ case "email":
+ return "email"
+ case "uri", "uri-reference":
+ return "url"
+ case "date":
+ return "date"
+ case "time":
+ return "time"
+ case "date-time":
+ return "datetime-local"
+ case "password":
+ return "password"
+ }
+ }
+
+ // Check type
+ var typeStr string
+ if len(schema.Type) > 0 {
+ typeStr = schema.Type[0]
+ }
+
+ switch typeStr {
+ case "string":
+ return "text"
+ case "number":
+ return "number"
+ case "integer":
+ return "number"
+ case "boolean":
+ return "checkbox"
+ default:
+ return "text"
+ }
+}
+
+// shouldUseTextarea determines if a string field should use textarea
+func shouldUseTextarea(schema *jsonschema.Schema) bool {
+ if schema.UI != nil {
+ if element, ok := schema.UI["element"].(string); ok {
+ return element == "textarea"
+ }
+ if rows, ok := schema.UI["rows"]; ok && rows != nil {
+ return true
+ }
+ }
+
+ // Use textarea for long strings
+ if schema.MaxLength != nil && *schema.MaxLength > 255 {
+ return true
+ }
+
+ return false
+}
+
+// shouldUseSelect determines if a field should use select element
+func shouldUseSelect(schema *jsonschema.Schema) bool {
+ if schema.UI != nil {
+ if element, ok := schema.UI["element"].(string); ok {
+ return element == "select"
+ }
+ }
+
+ // Use select for enum values
+ return len(schema.Enum) > 0
+}