Validation Practice & Exercises

8 minute read

Practice Overview

Apply your validation and error handling knowledge through hands-on exercises.

Exercise 1: Custom Email Validator

Task

Create a custom email validator with domain whitelist support.

Requirements:

  • Validate email format
  • Optional domain whitelist
  • Clear error messages

Starter Code

type emailValidator struct {
    allowedDomains []string
}

// TODO: Implement ValidateString
// TODO: Add Description methods

Solution

Click to reveal solution ```go type emailValidator struct { allowedDomains []string } func (v emailValidator) Description(ctx context.Context) string { if len(v.allowedDomains) > 0 { return fmt.Sprintf("value must be a valid email from domains: %v", v.allowedDomains) } return "value must be a valid email address" } func (v emailValidator) MarkdownDescription(ctx context.Context) string { return v.Description(ctx) } func (v emailValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { if req.ConfigValue.IsUnknown() || req.ConfigValue.IsNull() { return } value := req.ConfigValue.ValueString() // Basic email validation emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) if !emailRegex.MatchString(value) { resp.Diagnostics.AddAttributeError( req.Path, "Invalid Email Format", fmt.Sprintf("'%s' is not a valid email address. Format: user@example.com", value), ) return } // Check domain whitelist if provided if len(v.allowedDomains) > 0 { parts := strings.Split(value, "@") if len(parts) != 2 { return } domain := parts[1] validDomain := false for _, allowed := range v.allowedDomains { if domain == allowed { validDomain = true break } } if !validDomain { resp.Diagnostics.AddAttributeError( req.Path, "Invalid Email Domain", fmt.Sprintf( "Email domain '%s' is not allowed. Allowed domains: %v", domain, v.allowedDomains, ), ) } } } func Email(allowedDomains ...string) validator.String { return emailValidator{allowedDomains: allowedDomains} } // Usage "admin_email": schema.StringAttribute{ Required: true, Validators: []validator.String{ Email("example.com", "company.com"), }, }, ```

Exercise 2: Cross-Attribute Validation

Task

Implement validation that ensures TLS is enabled when public access is enabled.

Requirements:

  • Check if public_access is true
  • Require tls_enabled to be true when public_access is true
  • Provide clear error message

Starter Code

func (r *CacheResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
    var data CacheModel
    resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
    
    // TODO: Implement validation logic
}

Solution

Click to reveal solution ```go func (r *CacheResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { var data CacheModel resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } // Check if public access is enabled without TLS hasPublicAccess := !data.PublicAccess.IsNull() && data.PublicAccess.ValueBool() hasTLS := !data.TLSEnabled.IsNull() && data.TLSEnabled.ValueBool() if hasPublicAccess && !hasTLS { resp.Diagnostics.AddError( "Insecure Configuration", "TLS must be enabled when public access is enabled. "+ "Set 'tls_enabled = true' to encrypt data in transit. "+ "Learn more: https://docs.example.com/security/tls", ) } // Additional check: warn if TLS is disabled for any deployment if !data.TLSEnabled.IsNull() && !data.TLSEnabled.ValueBool() { resp.Diagnostics.AddWarning( "Security Recommendation", "TLS is not enabled. Consider enabling TLS to protect data in transit. "+ "Set 'tls_enabled = true'.", ) } } ```

Exercise 3: API Error Translation

Task

Create a helper function that translates API errors into user-friendly diagnostic messages.

Requirements:

  • Handle different error types (not found, rate limit, validation)
  • Return appropriate diagnostics
  • Include helpful context

Starter Code

func translateAPIError(err error, operation string) diag.Diagnostics {
    var diags diag.Diagnostics
    
    // TODO: Implement error translation
    
    return diags
}

Solution

Click to reveal solution ```go func translateAPIError(err error, operation, resourceID string) diag.Diagnostics { var diags diag.Diagnostics // Not found error var notFoundErr *NotFoundError if errors.As(err, &notFoundErr) { diags.AddError( fmt.Sprintf("%s Failed: Resource Not Found", operation), fmt.Sprintf( "The cache '%s' no longer exists. It may have been deleted outside of Terraform. "+ "Remove this resource from your configuration or run 'terraform apply' to recreate it.", resourceID, ), ) return diags } // Rate limit error var rateLimitErr *RateLimitError if errors.As(err, &rateLimitErr) { resetIn := time.Until(rateLimitErr.ResetAt).Round(time.Second) diags.AddError( "Rate Limit Exceeded", fmt.Sprintf( "API rate limit exceeded during %s operation. "+ "Current usage: %d/%d requests. "+ "Rate limit resets in %s (at %s). "+ "Terraform will automatically retry. If this persists, contact support.", operation, rateLimitErr.Limit-rateLimitErr.Remaining, rateLimitErr.Limit, resetIn, rateLimitErr.ResetAt.Format("15:04:05 MST"), ), ) return diags } // Validation error var validationErr *ValidationError if errors.As(err, &validationErr) { diags.AddAttributeError( path.Root(validationErr.Field), "Configuration Rejected by API", fmt.Sprintf( "The API rejected the value for '%s': %s. "+ "Update the configuration and try again.", validationErr.Field, validationErr.Reason, ), ) return diags } // Timeout error if errors.Is(err, context.DeadlineExceeded) { diags.AddError( fmt.Sprintf("%s Operation Timed Out", operation), fmt.Sprintf( "The %s operation for cache '%s' timed out. "+ "This may be due to network issues or API slowness. "+ "The operation may still complete in the background. "+ "Wait a few minutes and check the cache status, or try again.", operation, resourceID, ), ) return diags } // Generic API error var apiErr *APIError if errors.As(err, &apiErr) { diags.AddError( fmt.Sprintf("%s Failed", operation), fmt.Sprintf( "API request failed with status %d: %s. "+ "Request ID: %s. "+ "If this error persists, contact support with the request ID.", apiErr.StatusCode, apiErr.Message, apiErr.RequestID, ), ) return diags } // Unknown error diags.AddError( fmt.Sprintf("%s Failed", operation), fmt.Sprintf( "An unexpected error occurred during %s: %s. "+ "Check your network connectivity and API credentials. "+ "If the problem persists, contact support.", operation, err.Error(), ), ) return diags } // Usage in resource methods func (r *CacheResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var data CacheModel resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) cache, err := r.client.CreateCache(ctx, input) if err != nil { resp.Diagnostics.Append(translateAPIError(err, "Creation", data.Name.ValueString())...) return } // Success } ```

Exercise 4: Retry Logic

Task

Implement retry logic with exponential backoff for API calls.

Requirements:

  • Retry only retryable errors
  • Use exponential backoff
  • Add jitter
  • Respect context timeout

Solution

Click to reveal solution ```go type RetryConfig struct { MaxRetries int InitialBackoff time.Duration MaxBackoff time.Duration BackoffFactor float64 } func retryWithBackoff(ctx context.Context, config RetryConfig, fn func() error) error { backoff := config.InitialBackoff for attempt := 0; attempt <= config.MaxRetries; attempt++ { err := fn() // Success if err == nil { return nil } // Non-retryable error if !IsRetryableError(err) { return err } // Last attempt if attempt == config.MaxRetries { return fmt.Errorf("max retries (%d) exceeded: %w", config.MaxRetries, err) } // Calculate backoff with jitter (±30%) jitter := time.Duration(rand.Float64() * float64(backoff) * 0.6) - time.Duration(float64(backoff)*0.3) sleepDuration := backoff + jitter // Ensure non-negative sleep if sleepDuration < 0 { sleepDuration = backoff } // Sleep with context awareness select { case <-time.After(sleepDuration): // Continue to next attempt case <-ctx.Done(): return ctx.Err() } // Increase backoff for next attempt backoff = time.Duration(float64(backoff) * config.BackoffFactor) if backoff > config.MaxBackoff { backoff = config.MaxBackoff } } return fmt.Errorf("unexpected retry loop exit") } // Usage func (c *Client) CreateCache(ctx context.Context, input CreateCacheInput) (*Cache, error) { var cache *Cache var lastErr error config := RetryConfig{ MaxRetries: 3, InitialBackoff: 1 * time.Second, MaxBackoff: 30 * time.Second, BackoffFactor: 2.0, } err := retryWithBackoff(ctx, config, func() error { var err error cache, err = c.createCacheInternal(ctx, input) lastErr = err return err }) if err != nil { return nil, err } return cache, nil } ```

Challenge Problems

Challenge 1: Complex Validation

Create validation for a cache configuration that requires:

  • Backup must be enabled for production environments
  • Multi-AZ requires at least 2 availability zones
  • Encryption at rest required for compliance mode
  • TLS required for public access

Challenge 2: Error Recovery

Implement error recovery for a scenario where:

  • Resource creation succeeds but becomes unavailable
  • Need to save partial state
  • Provide clear guidance to user
  • Handle cleanup if needed

Challenge 3: Rate Limit Strategy

Implement smart rate limiting that:

  • Tracks API rate limit headers
  • Proactively throttles requests
  • Queues requests when approaching limits
  • Provides feedback to users

Quiz Questions

Question 1

When should you use AddError vs AddWarning?

Answer **Use AddError when:** - Configuration is invalid and cannot proceed - Required attributes are missing - API returns unrecoverable errors - Operation must be blocked **Use AddWarning when:** - Configuration is valid but suboptimal - Features are deprecated - Best practices are not followed - Non-blocking recommendations Example: ```go // Error: Blocks operation resp.Diagnostics.AddError( "Missing Required Attribute", "The 'engine' attribute is required", ) // Warning: Allows operation resp.Diagnostics.AddWarning( "Deprecated Attribute", "The 'legacy_mode' attribute is deprecated", ) ```

Question 2

What’s the difference between schema validation and plan-time validation?

Answer **Schema Validation:** - Validates individual attributes - Runs during plan phase - Cannot access other attributes - Simple rules (format, range, enum) - Examples: string length, number range, regex **Plan-Time Validation (ValidateConfig):** - Validates across multiple attributes - Runs during plan phase - Can access all configuration - Complex rules (cross-attribute, business logic) - Examples: conflicting settings, dependent attributes ```go // Schema validation: Single attribute "port": schema.Int64Attribute{ Validators: []validator.Int64{ int64validator.Between(1024, 65535), }, } // Plan-time validation: Multiple attributes func (r *Resource) ValidateConfig(...) { if data.PublicAccess.ValueBool() && !data.TLS.ValueBool() { // Error: TLS required for public access } } ```

Question 3

How should you handle retryable vs non-retryable errors?

Answer **Retryable Errors:** - Network timeouts - Rate limiting (429) - Server errors (5xx) - Temporary failures **Non-Retryable Errors:** - Client errors (4xx except 429) - Authentication failures (401, 403) - Not found (404) - Validation errors (400) ```go func IsRetryableError(err error) bool { // Network errors var netErr net.Error if errors.As(err, &netErr) && netErr.Temporary() { return true } // Rate limits if IsRateLimitError(err) { return true } // Server errors var apiErr *APIError if errors.As(err, &apiErr) { return apiErr.StatusCode >= 500 } return false } // Implementation if IsRetryableError(err) { return retryWithBackoff(ctx, fn) } else { return err // Don't retry } ```

Best Practices Checklist

Ensure your validation and error handling follows these practices:

  • ✅ Validate at the earliest possible stage
  • ✅ Use schema validators for simple rules
  • ✅ Use ValidateConfig for complex rules
  • ✅ Provide clear, actionable error messages
  • ✅ Include examples in validation messages
  • ✅ Point errors to specific attributes
  • ✅ Use warnings for non-blocking issues
  • ✅ Classify errors (retryable, permanent, transient)
  • ✅ Implement exponential backoff for retries
  • ✅ Respect rate limits and Retry-After headers
  • ✅ Include request IDs in error messages
  • ✅ Handle timeouts gracefully
  • ✅ Translate API errors to user-friendly messages
  • ✅ Test validation with invalid inputs
  • ✅ Test error handling paths

Additional Resources

Documentation

Example Providers

Study validation patterns in:

Key Takeaways

  • ✅ Practice implementing custom validators
  • ✅ Master cross-attribute validation
  • ✅ Write user-friendly error messages
  • ✅ Handle API errors gracefully
  • ✅ Implement proper retry logic
  • ✅ Classify errors appropriately
  • ✅ Test validation thoroughly
  • ✅ Use warnings for recommendations
  • ✅ Provide actionable guidance
  • ✅ Follow validation best practices

What’s Next?

Continue to Module 12 to learn about documentation generation for your provider.

Next Module: Documentation Generation →


Previous: ← API Error Handling

Last updated: June 30, 2026

Updated: