This commit is contained in:
sujit
2025-08-05 11:27:15 +05:45
parent c55b34f273
commit 8d2662d9c0
6 changed files with 2741 additions and 46 deletions

View File

@@ -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
}
]
}

View File

@@ -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"
}
}
}

View File

@@ -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.

View File

@@ -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 := `
<form {{if .Form.Action}}action="{{.Form.Action}}"{{end}} {{if .Form.Method}}method="{{.Form.Method}}"{{end}} {{if .Form.Class}}class="{{.Form.Class}}"{{end}}>
{{.FieldsHTML}}
<div class="form-buttons">
{{.ButtonsHTML}}
</div>
</form>
`
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 := "<!DOCTYPE html>" +
"<html>" +
"<head>" +
"<title>JSON Schema Form Builder - Validation Examples</title>" +
"<style>" +
"body { font-family: Arial, sans-serif; margin: 40px; }" +
"h1 { color: #333; }" +
".example-list { list-style: none; padding: 0; }" +
".example-list li { margin: 15px 0; }" +
".example-list a { display: inline-block; padding: 10px 20px; background: #007bff; color: white; text-decoration: none; border-radius: 5px; }" +
".example-list a:hover { background: #0056b3; }" +
".description { color: #666; margin-top: 5px; }" +
"</style>" +
"</head>" +
"<body>" +
"<h1>JSON Schema Form Builder - Validation Examples</h1>" +
"<p>This demo showcases comprehensive JSON Schema 2020-12 validation support with:</p>" +
"<ul>" +
"<li>String validations (minLength, maxLength, pattern, format)</li>" +
"<li>Numeric validations (minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf)</li>" +
"<li>Array validations (minItems, maxItems, uniqueItems)</li>" +
"<li>Conditional validations (if/then/else, allOf, anyOf, oneOf)</li>" +
"<li>Dependent validations (dependentRequired, dependentSchemas)</li>" +
"<li>Client-side and server-side validation</li>" +
"<li>Advanced HTML form generation with validation attributes</li>" +
"</ul>" +
"<h2>Available Examples:</h2>" +
"<ul class=\"example-list\">" +
"<li>" +
"<a href=\"/comprehensive\">Comprehensive Validation Demo</a>" +
"<div class=\"description\">Complete example with personal info, address, preferences, financial data, skills, portfolio, and complex conditional logic</div>" +
"</li>" +
"<li>" +
"<a href=\"/features\">Validation Features Demo</a>" +
"<div class=\"description\">Focused examples of specific validation features organized by category</div>" +
"</li>" +
"<li>" +
"<a href=\"/complex\">Complex Form Demo</a>" +
"<div class=\"description\">Original company registration form with nested objects and grouped fields</div>" +
"</li>" +
"</ul>" +
"</body>" +
"</html>"
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(html))
}
func createFullHTMLPage(title, formHTML string) string {
template := "<!DOCTYPE html>" +
"<html>" +
"<head>" +
"<title>" + title + "</title>" +
"<meta charset=\"utf-8\">" +
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">" +
"<script src=\"https://cdn.tailwindcss.com\"></script>" +
"<style>" +
".form-group { margin-bottom: 1rem; }" +
".form-group label { display: block; margin-bottom: 0.25rem; font-weight: 600; }" +
".form-group input, .form-group select, .form-group textarea { width: 100%; padding: 0.5rem; border: 1px solid #d1d5db; border-radius: 0.375rem; }" +
".btn { padding: 0.5rem 1rem; border-radius: 0.375rem; font-weight: 600; cursor: pointer; border: none; margin-right: 0.5rem; }" +
".btn-primary { background-color: #3b82f6; color: white; }" +
".validation-error { color: #dc2626; font-size: 0.875rem; margin-top: 0.25rem; }" +
".back-link { display: inline-block; margin-bottom: 2rem; color: #3b82f6; text-decoration: none; }" +
"</style>" +
"</head>" +
"<body class=\"bg-gray-50 min-h-screen py-8\">" +
"<div class=\"max-w-4xl mx-auto px-4\">" +
"<a href=\"/\" class=\"back-link\">← Back to Examples</a>" +
"<div class=\"bg-white rounded-lg shadow-lg p-8\">" +
"<h1 class=\"text-3xl font-bold text-gray-900 mb-6\">" + title + "</h1>" +
"<p class=\"text-gray-600 mb-8\">This form demonstrates comprehensive JSON Schema validation.</p>" +
formHTML +
"</div></div>" +
"<script>" +
"document.addEventListener('DOMContentLoaded', function() {" +
"const form = document.querySelector('form');" +
"if (form) {" +
"form.addEventListener('submit', function(e) {" +
"e.preventDefault();" +
"alert('Form validation demo - would submit data in real application');" +
"});" +
"}" +
"});" +
"</script>" +
"</body></html>"
return template
}

View File

@@ -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
@@ -179,15 +206,77 @@ type GroupTitle struct {
type JSONSchemaRenderer struct {
Schema *jsonschema.Schema
HTMLLayout string
cachedHTML string // Cached rendered HTML
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,
}
// 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
}
// 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)
}
buttonsHTML := r.renderButtons()
return renderedHTML.String(), nil
}
// 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))
}
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(`<option value="%s"%s%s>%s</option>`,
value, selected, disabled, text))
} else {
optionsHTML.WriteString(fmt.Sprintf(`<option value="%v">%v</option>`, option, option))
}
}
return optionsHTML.String()
}
}
// Generate options from enum
if len(schema.Enum) > 0 {
for _, enumValue := range schema.Enum {
optionsHTML.WriteString(fmt.Sprintf(`<option value="%v">%v</option>`, 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
}

663
renderer/validation.go Normal file
View File

@@ -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(`
<script>
(function() {
var field = document.querySelector('[name="%s"]');
if (!field) return;
function validate%s(value) {
%s
return null;
}
function validateForm() {
// Dependent validations that check other fields
%s
return null;
}
field.addEventListener('blur', function() {
var error = validate%s(this.value) || validateForm();
var errorElement = document.getElementById('%s_error');
if (error) {
if (!errorElement) {
errorElement = document.createElement('div');
errorElement.id = '%s_error';
errorElement.className = 'validation-error';
errorElement.style.color = 'red';
errorElement.style.fontSize = '0.875rem';
errorElement.style.marginTop = '0.25rem';
this.parentNode.appendChild(errorElement);
}
errorElement.textContent = error;
this.classList.add('invalid');
this.style.borderColor = 'red';
} else {
if (errorElement) {
errorElement.remove();
}
this.classList.remove('invalid');
this.style.borderColor = '';
}
});
// Also validate on input for immediate feedback
field.addEventListener('input', function() {
if (this.classList.contains('invalid')) {
var error = validate%s(this.value);
var errorElement = document.getElementById('%s_error');
if (!error && errorElement) {
errorElement.remove();
this.classList.remove('invalid');
this.style.borderColor = '';
}
}
});
})();
</script>
`, 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
}