This commit is contained in:
sujit
2025-08-05 12:06:42 +05:45
parent 8d2662d9c0
commit 4b28828f24
4 changed files with 74 additions and 520 deletions

View File

@@ -12,6 +12,7 @@
"ui": { "ui": {
"element": "input", "element": "input",
"type": "text", "type": "text",
"name": "name",
"class": "form-group", "class": "form-group",
"order": 1 "order": 1
} }

View File

@@ -1,103 +0,0 @@
# 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

@@ -1,173 +0,0 @@
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

@@ -481,8 +481,24 @@ func (r *JSONSchemaRenderer) extractFieldsFromPath(fieldPath, parentPath string)
return fields return fields
} }
// Check if this field is required // For nested paths like "company.address.street", we need to check if the final part
isRequired := r.isFieldRequired(fieldPath) // is required in its immediate parent's schema
var fieldName string
var parentSchemaPath string
pathParts := strings.Split(fieldPath, ".")
if len(pathParts) > 1 {
// Extract the field name (last part) and parent path (all but last)
fieldName = pathParts[len(pathParts)-1]
parentSchemaPath = strings.Join(pathParts[:len(pathParts)-1], ".")
} else {
// Single level field
fieldName = fieldPath
parentSchemaPath = parentPath
}
// Check if this field is required at the parent level
isRequired := r.isFieldRequiredAtPath(fieldName, parentSchemaPath)
// If this schema has properties, it's a nested object // If this schema has properties, it's a nested object
if schema.Properties != nil && len(*schema.Properties) > 0 { if schema.Properties != nil && len(*schema.Properties) > 0 {
@@ -499,7 +515,7 @@ func (r *JSONSchemaRenderer) extractFieldsFromPath(fieldPath, parentPath string)
} }
fields = append(fields, FieldInfo{ fields = append(fields, FieldInfo{
Name: fieldPath, Name: fieldName,
FieldPath: fullPath, FieldPath: fullPath,
Order: order, Order: order,
Schema: schema, Schema: schema,
@@ -512,7 +528,7 @@ func (r *JSONSchemaRenderer) extractFieldsFromPath(fieldPath, parentPath string)
} }
// extractFieldsFromNestedSchema processes nested schema properties // extractFieldsFromNestedSchema processes nested schema properties
func (r *JSONSchemaRenderer) extractFieldsFromNestedSchema(propName, parentPath string, propSchema *jsonschema.Schema, parentRequired bool) []FieldInfo { func (r *JSONSchemaRenderer) extractFieldsFromNestedSchema(propName, parentPath string, propSchema *jsonschema.Schema, _ bool) []FieldInfo {
var fields []FieldInfo var fields []FieldInfo
fullPath := propName fullPath := propName
@@ -520,8 +536,9 @@ func (r *JSONSchemaRenderer) extractFieldsFromNestedSchema(propName, parentPath
fullPath = parentPath + "." + propName fullPath = parentPath + "." + propName
} }
// Check if this nested field is required // Check if this field is required at its immediate parent level
isRequired := parentRequired || contains(r.Schema.Required, propName) // The parent schema path is the current parentPath, not one level up
isRequired := r.isFieldRequiredAtPath(propName, parentPath)
// If this property has nested properties, recurse // If this property has nested properties, recurse
if propSchema.Properties != nil && len(*propSchema.Properties) > 0 { if propSchema.Properties != nil && len(*propSchema.Properties) > 0 {
@@ -558,24 +575,57 @@ func (r *JSONSchemaRenderer) getSchemaAtPath(path string) *jsonschema.Schema {
parts := strings.Split(path, ".") parts := strings.Split(path, ".")
currentSchema := r.Schema currentSchema := r.Schema
for _, part := range parts { fmt.Printf("DEBUG: Navigating to path '%s', parts: %v\n", path, parts)
for i, part := range parts {
if currentSchema.Properties == nil { if currentSchema.Properties == nil {
fmt.Printf("DEBUG: No properties at part %d ('%s')\n", i, part)
return nil return nil
} }
if propSchema, exists := (*currentSchema.Properties)[part]; exists { if propSchema, exists := (*currentSchema.Properties)[part]; exists {
currentSchema = propSchema currentSchema = propSchema
fmt.Printf("DEBUG: Found part '%s' at level %d\n", part, i)
} else { } else {
fmt.Printf("DEBUG: Part '%s' not found at level %d. Available: %v\n", part, i, func() []string {
var keys []string
for k := range *currentSchema.Properties {
keys = append(keys, k)
}
return keys
}())
return nil return nil
} }
} }
fmt.Printf("DEBUG: Successfully navigated to path '%s', found required: %v\n", path, currentSchema.Required)
return currentSchema return currentSchema
} }
// isFieldRequired checks if a field is required at the current schema level // isFieldRequiredAtPath checks if a field is required at a specific schema path
func (r *JSONSchemaRenderer) isFieldRequired(fieldName string) bool { func (r *JSONSchemaRenderer) isFieldRequiredAtPath(fieldName, schemaPath string) bool {
return contains(r.Schema.Required, fieldName) var schema *jsonschema.Schema
if schemaPath == "" {
// Check at root level
schema = r.Schema
} else {
// Navigate to the schema at the given path
schema = r.getSchemaAtPath(schemaPath)
}
if schema == nil {
// Debug: schema not found
fmt.Printf("DEBUG: Schema not found for path '%s'\n", schemaPath)
return false
}
isRequired := contains(schema.Required, fieldName)
// Debug: show what we're checking
fmt.Printf("DEBUG: Checking if '%s' is required in schema at path '%s'. Required fields: %v. Result: %v\n",
fieldName, schemaPath, schema.Required, isRequired)
return isRequired
} }
// renderGroup generates HTML for a single group // renderGroup generates HTML for a single group
@@ -736,11 +786,21 @@ func buildAllAttributesWithValidation(field FieldInfo) string {
var builder strings.Builder var builder strings.Builder
builder.Grow(512) // Pre-allocate capacity builder.Grow(512) // Pre-allocate capacity
// Use the field path as the name attribute for nested fields // Check if UI specifies a custom name, otherwise use field path for nested fields
fieldName := field.FieldPath var fieldName string
if field.Schema.UI != nil {
if customName, exists := field.Schema.UI["name"].(string); exists && customName != "" {
fieldName = customName
}
}
// Fallback to field path or field name
if fieldName == "" {
fieldName = field.FieldPath
if fieldName == "" { if fieldName == "" {
fieldName = field.Name fieldName = field.Name
} }
}
// Add name attribute // Add name attribute
builder.WriteString(`name="`) builder.WriteString(`name="`)
@@ -895,184 +955,6 @@ func getPlaceholder(schema *jsonschema.Schema) string {
return "" 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
// Use the field path as the name attribute for nested fields
fieldName := field.FieldPath
if fieldName == "" {
fieldName = field.Name
}
// Add name attribute
attributes = append(attributes, fmt.Sprintf(`name="%s"`, fieldName))
// 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") {
attributes = append(attributes, fmt.Sprintf(`class="%v"`, value))
continue
}
// For other elements, do not add class here (it will be handled in the wrapper div)
if attr == "class" {
continue
}
attributes = append(attributes, fmt.Sprintf(`%s="%v"`, attr, value))
}
}
// Add data-* and aria-* attributes
for key, value := range field.Schema.UI {
if strings.HasPrefix(key, "data-") || strings.HasPrefix(key, "aria-") {
attributes = append(attributes, fmt.Sprintf(`%s="%v"`, key, value))
}
}
}
// Handle required field
if field.IsRequired {
if !containsAttribute(attributes, "required=") {
attributes = append(attributes, `required="required"`)
}
}
// Handle input type based on field type
element, _ := field.Schema.UI["element"].(string)
if element == "input" {
if inputType := getInputType(field.Schema); inputType != "" {
if !containsAttribute(attributes, "type=") {
attributes = append(attributes, fmt.Sprintf(`type="%s"`, inputType))
}
}
}
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 {
if uiType, ok := schema.UI["type"].(string); ok {
return uiType
}
}
var typeStr string
if len(schema.Type) > 0 {
typeStr = schema.Type[0]
} else {
typeStr = "string"
}
// Map schema types to input types
switch typeStr {
case "string", "text":
return "text"
case "number", "integer":
return "number"
case "boolean":
return "checkbox"
default:
return "text"
}
}
func getFieldContent(field FieldInfo) string { func getFieldContent(field FieldInfo) string {
// Check for content in UI first // Check for content in UI first
if field.Schema.UI != nil { if field.Schema.UI != nil {
@@ -1157,50 +1039,6 @@ func generateLabel(field FieldInfo) string {
return fmt.Sprintf(`<label for="%s">%s%s</label>`, fieldName, title, requiredSpan) return fmt.Sprintf(`<label for="%s">%s%s</label>`, fieldName, title, requiredSpan)
} }
func generateOptions(ui map[string]interface{}) string {
if ui == nil {
return ""
}
options, ok := ui["options"].([]interface{})
if !ok {
return ""
}
var optionsHTML strings.Builder
for _, option := range options {
if optionMap, ok := option.(map[string]interface{}); ok {
// Complex option with attributes
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 {
// Simple option (just value)
optionsHTML.WriteString(fmt.Sprintf(`<option value="%v">%v</option>`, option, option))
}
}
return optionsHTML.String()
}
func getUIValue(ui map[string]interface{}, key string) string {
if ui == nil {
return ""
}
if value, ok := ui[key].(string); ok {
return value
}
return ""
}
func getMapValue(m map[string]interface{}, key, defaultValue string) string { func getMapValue(m map[string]interface{}, key, defaultValue string) string {
if value, ok := m[key].(string); ok { if value, ok := m[key].(string); ok {
return value return value
@@ -1217,15 +1055,6 @@ func contains(slice []string, item string) bool {
return false return false
} }
func containsAttribute(attributes []string, prefix string) bool {
for _, attr := range attributes {
if strings.HasPrefix(attr, prefix) {
return true
}
}
return false
}
// renderButtons generates HTML for form buttons // renderButtons generates HTML for form buttons
func (r *JSONSchemaRenderer) renderButtons() string { func (r *JSONSchemaRenderer) renderButtons() string {
if r.Schema.Form == nil { if r.Schema.Form == nil {