Skip to content

Validation

The H5P Go SDK includes comprehensive validation to ensure your content meets H5P standards and functions correctly across platforms.

Overview

Validation occurs at multiple levels: - Parameter validation - Field types, required values, constraints - Structure validation - Proper relationships between components - Schema compliance - Conformance with official H5P specifications - Business logic validation - Sensible values and configurations

Validation Levels

Content Validation

// Validate question set parameters
questionSet, err := builder.Build()
if err != nil {
    // Build-time validation failed
    log.Fatal("Build failed:", err)
}

// Runtime validation
if err := questionSet.Validate(); err != nil {
    // Content validation failed
    log.Fatal("Validation failed:", err)
}

Schema Validation

import "github.com/grokify/h5p-go/schemas"

// Validate typed parameters
params := &schemas.MultiChoiceParams{
    Question: "What is 2+2?",
    Answers: []schemas.AnswerOption{
        {Text: "4", Correct: true},
    },
}

if err := params.Validate(); err != nil {
    log.Fatal("Schema validation failed:", err)
}

Package Validation

// Validate complete H5P package
pkg := h5p.NewH5PPackage()
// ... configure package ...

if err := pkg.Validate(); err != nil {
    log.Fatal("Package validation failed:", err)
}

Common Validation Rules

Question Sets

Required Fields: - Title must not be empty - At least one question required - Pass percentage between 0-100

Business Logic: - Pass percentage should be reasonable (typically 50-90) - Questions should have meaningful text - Feedback ranges should not overlap

// Valid question set
questionSet, err := h5p.NewQuestionSetBuilder().
    SetTitle("My Quiz").              // Required
    SetPassPercentage(70).           // 0-100
    AddMultipleChoiceQuestion(       // At least 1 question
        "Question text?", 
        answers,
    ).
    Build()

Multiple Choice Questions

Required Fields: - Question text must not be empty - At least 2 answers required - At least 1 correct answer required

Constraints: - Maximum answer length limits - Feedback text length limits - Pass percentage 0-100

// Valid multiple choice
params := &schemas.MultiChoiceParams{
    Question: "What is the capital?",  // Required, non-empty
    Answers: []schemas.AnswerOption{   // Min 2 answers
        {Text: "Paris", Correct: true},     // At least 1 correct
        {Text: "London", Correct: false},   // Text required
    },
}

if err := params.Validate(); err != nil {
    // Validation failed
}

Answer Options

Rules: - Text field is required and non-empty - At least one answer must be marked correct - Feedback text has length limits

// Valid answer
answer := schemas.AnswerOption{
    Text:    "Correct Answer",        // Required
    Correct: true,                    // At least 1 must be true
    TipsAndFeedback: &schemas.AnswerTipsAndFeedback{
        ChosenFeedback: "Good job!",  // Optional, but limited length
    },
}

Error Types

Validation Errors

The SDK provides structured validation errors:

type ValidationError struct {
    Field   string `json:"field"`
    Message string `json:"message"`
    Value   interface{} `json:"value,omitempty"`
}

func (e ValidationError) Error() string {
    return fmt.Sprintf("validation error on field '%s': %s", e.Field, e.Message)
}

Multiple Errors

Validation can return multiple errors at once:

type ValidationErrors []ValidationError

func (e ValidationErrors) Error() string {
    messages := make([]string, len(e))
    for i, err := range e {
        messages[i] = err.Error()
    }
    return strings.Join(messages, "; ")
}

Error Handling

questionSet, err := builder.Build()
if err != nil {
    if validationErrors, ok := err.(h5p.ValidationErrors); ok {
        fmt.Println("Multiple validation errors:")
        for _, validationError := range validationErrors {
            fmt.Printf("- %s: %s\n", validationError.Field, validationError.Message)
        }
    } else {
        fmt.Printf("Build error: %s\n", err)
    }
    return
}

Custom Validation

Adding Custom Rules

You can implement custom validation for specific use cases:

func validateQuizComplexity(questionSet *h5p.QuestionSet) error {
    if len(questionSet.Questions) < 3 {
        return fmt.Errorf("quiz should have at least 3 questions for proper assessment")
    }

    // Check for variety in answer counts
    answerCounts := make(map[int]int)
    for _, question := range questionSet.Questions {
        if params, ok := question.Params.(map[string]interface{}); ok {
            if answers, exists := params["answers"]; exists {
                if answerList, ok := answers.([]interface{}); ok {
                    answerCounts[len(answerList)]++
                }
            }
        }
    }

    if len(answerCounts) == 1 {
        return fmt.Errorf("consider varying the number of answers for better engagement")
    }

    return nil
}

// Use custom validation
if err := validateQuizComplexity(questionSet); err != nil {
    log.Printf("Warning: %s", err)
}

Validation Best Practices

1. Validate Early and Often

// Validate at build time
questionSet, err := builder.Build()
if err != nil {
    return err  // Fail fast
}

// Validate before export
if err := questionSet.Validate(); err != nil {
    return err
}

// Validate before package creation
if err := pkg.Validate(); err != nil {
    return err
}

2. Handle Validation Gracefully

func buildQuestionSet(title string, questions []QuestionData) (*h5p.QuestionSet, error) {
    builder := h5p.NewQuestionSetBuilder().SetTitle(title)

    for i, q := range questions {
        if q.Text == "" {
            return nil, fmt.Errorf("question %d has empty text", i+1)
        }

        if len(q.Answers) < 2 {
            return nil, fmt.Errorf("question %d needs at least 2 answers", i+1)
        }

        // Convert and add question
        answers := convertAnswers(q.Answers)
        builder = builder.AddMultipleChoiceQuestion(q.Text, answers)
    }

    return builder.Build()
}

3. Provide Meaningful Error Messages

func validatePassPercentage(percentage int) error {
    if percentage < 0 || percentage > 100 {
        return fmt.Errorf("pass percentage must be between 0 and 100, got %d", percentage)
    }

    if percentage < 30 {
        return fmt.Errorf("pass percentage %d seems too low - consider 50-80 range", percentage)
    }

    if percentage > 95 {
        return fmt.Errorf("pass percentage %d seems too high - consider 60-90 range", percentage)
    }

    return nil
}

Testing Validation

Unit Testing

func TestQuestionSetValidation(t *testing.T) {
    tests := []struct {
        name      string
        builder   func() *h5p.QuestionSetBuilder
        wantError bool
        errorText string
    }{
        {
            name: "valid question set",
            builder: func() *h5p.QuestionSetBuilder {
                return h5p.NewQuestionSetBuilder().
                    SetTitle("Test Quiz").
                    SetPassPercentage(70).
                    AddMultipleChoiceQuestion("Question?", validAnswers)
            },
            wantError: false,
        },
        {
            name: "missing title",
            builder: func() *h5p.QuestionSetBuilder {
                return h5p.NewQuestionSetBuilder().
                    SetPassPercentage(70).
                    AddMultipleChoiceQuestion("Question?", validAnswers)
            },
            wantError: true,
            errorText: "title is required",
        },
        {
            name: "invalid pass percentage",
            builder: func() *h5p.QuestionSetBuilder {
                return h5p.NewQuestionSetBuilder().
                    SetTitle("Test Quiz").
                    SetPassPercentage(150).  // Invalid
                    AddMultipleChoiceQuestion("Question?", validAnswers)
            },
            wantError: true,
            errorText: "pass percentage must be between 0 and 100",
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            _, err := tt.builder().Build()

            if tt.wantError {
                if err == nil {
                    t.Errorf("expected error but got none")
                    return
                }
                if !strings.Contains(err.Error(), tt.errorText) {
                    t.Errorf("error %q should contain %q", err.Error(), tt.errorText)
                }
            } else {
                if err != nil {
                    t.Errorf("unexpected error: %v", err)
                }
            }
        })
    }
}

Integration Testing

func TestCompleteWorkflow(t *testing.T) {
    // Test the complete workflow with validation
    questionSet, err := createTestQuestionSet()
    if err != nil {
        t.Fatalf("Failed to create question set: %v", err)
    }

    // Validate content
    if err := questionSet.Validate(); err != nil {
        t.Fatalf("Question set validation failed: %v", err)
    }

    // Create package
    pkg := h5p.NewH5PPackage()
    pkg.SetContent(&h5p.Content{Params: questionSet})

    // Validate package
    if err := pkg.Validate(); err != nil {
        t.Fatalf("Package validation failed: %v", err)
    }

    // Export should succeed
    tempFile := filepath.Join(t.TempDir(), "test.h5p")
    if err := pkg.CreateZipFile(tempFile); err != nil {
        t.Fatalf("Failed to export package: %v", err)
    }

    // Re-load and validate
    loadedPkg, err := h5p.LoadH5PPackage(tempFile)
    if err != nil {
        t.Fatalf("Failed to load package: %v", err)
    }

    if err := loadedPkg.Validate(); err != nil {
        t.Fatalf("Loaded package validation failed: %v", err)
    }
}

Validation Checklist

Before deploying H5P content, ensure:

  • [ ] All required fields are populated
  • [ ] Field values are within acceptable ranges
  • [ ] Questions have meaningful text
  • [ ] At least one correct answer per question
  • [ ] Pass percentage is reasonable (50-90)
  • [ ] Feedback messages are helpful and appropriate
  • [ ] Package includes all required libraries
  • [ ] Library versions are compatible
  • [ ] No validation errors or warnings

Next Steps