JSON Views
Overview
JSON Views are responsible for transforming Maestro output data into structured JSON responses for API endpoints. They handle data serialization, format consistency, and provide clean interfaces between business logic and API consumers.
Purpose
JSON Views serve several key functions: - Data Serialization - Transform entities and data into JSON format - Response Structure - Provide consistent API response formats - Data Filtering - Control which fields are included in responses - Error Formatting - Structure error responses consistently - API Versioning Support - Handle different API response formats
Structure
Each JSON view typically follows this pattern:
type ListPlantsJsonView struct {
PlantSerializer *serializers.PlantSerializer
Kit *kit.Kit
}
func (this *ListPlantsJsonView) H(out *maes.ListPlantsMaeOut) (map[string]interface{}, error) {
response := make(map[string]interface{})
// Serialize main data
plants, err := this.PlantSerializer.SerializeMany(out.Plants, "list")
if err != nil {
return nil, fmt.Errorf("failed to serialize plants: %w", err)
}
response["plants"] = plants
response["pagination"] = this.serializePagination(out.Pagination)
return response, nil
}
Core Methods
Main Serialization Method
func (this *ShowPlantJsonView) H(out *maes.ShowPlantMaeOut) (map[string]interface{}, error) {
plant, err := this.PlantSerializer.SerializeOne(out.Plant, "show")
if err != nil {
return nil, fmt.Errorf("failed to serialize plant: %w", err)
}
response := map[string]interface{}{
"plant": plant,
}
if out.RelatedData != nil {
response["related"] = this.serializeRelatedData(out.RelatedData)
}
return response, nil
}
Collection Serialization
func (this *ListPlantsJsonView) serializePlants(plants []*mdl.Plant) ([]map[string]interface{}, error) {
var serialized []map[string]interface{}
for _, plant := range plants {
plantData, err := this.PlantSerializer.SerializeOne(plant, "list")
if err != nil {
return nil, fmt.Errorf("failed to serialize plant %s: %w", plant.Id, err)
}
serialized = append(serialized, plantData)
}
return serialized, nil
}
Metadata Serialization
func (this *ListPlantsJsonView) serializePagination(pagination *common.Pagination) map[string]interface{} {
if pagination == nil {
return nil
}
return map[string]interface{}{
"current_page": pagination.CurrentPage,
"per_page": pagination.PerPage,
"total": pagination.Total,
"total_pages": pagination.TotalPages,
"has_next": pagination.HasNext,
"has_prev": pagination.HasPrev,
}
}
Response Patterns
Success Responses
// Single entity response
func (this *ShowPlantJsonView) H(out *maes.ShowPlantMaeOut) (map[string]interface{}, error) {
plant, err := this.PlantSerializer.SerializeOne(out.Plant, "show")
if err != nil {
return nil, err
}
return map[string]interface{}{
"success": true,
"data": plant,
}, nil
}
// Collection response
func (this *ListPlantsJsonView) H(out *maes.ListPlantsMaeOut) (map[string]interface{}, error) {
plants, err := this.PlantSerializer.SerializeMany(out.Plants, "list")
if err != nil {
return nil, err
}
return map[string]interface{}{
"success": true,
"data": plants,
"pagination": this.serializePagination(out.Pagination),
}, nil
}
Error Responses
func (this *JsonViewHelper) ErrorResponse(err error, code string) map[string]interface{} {
response := map[string]interface{}{
"success": false,
"error": map[string]interface{}{
"message": err.Error(),
"code": code,
},
}
// Add validation errors if available
if validationErr, ok := err.(*ValidationError); ok {
response["error"].(map[string]interface{})["validation"] = validationErr.Fields
}
return response
}
Nested Data Serialization
func (this *ShowGardenJsonView) H(out *maes.ShowGardenMaeOut) (map[string]interface{}, error) {
garden, err := this.GardenSerializer.SerializeOne(out.Garden, "show")
if err != nil {
return nil, err
}
response := map[string]interface{}{
"garden": garden,
}
// Include related plants if hydrated
if len(out.Garden.Plants) > 0 {
plants, err := this.PlantSerializer.SerializeMany(out.Garden.Plants, "embedded")
if err != nil {
return nil, fmt.Errorf("failed to serialize plants: %w", err)
}
response["garden"].(map[string]interface{})["plants"] = plants
}
// Include user information if available
if out.Garden.User != nil {
user, err := this.UserSerializer.SerializeOne(out.Garden.User, "basic")
if err != nil {
return nil, fmt.Errorf("failed to serialize user: %w", err)
}
response["garden"].(map[string]interface{})["user"] = user
}
return response, nil
}
Serialization Contexts
Context-Based Serialization
func (this *PlantJsonView) serializePlant(plant *mdl.Plant, context string) (map[string]interface{}, error) {
switch context {
case "list":
return this.PlantSerializer.SerializeOne(plant, "list")
case "show":
return this.PlantSerializer.SerializeOne(plant, "show")
case "embedded":
return this.PlantSerializer.SerializeOne(plant, "embedded")
default:
return this.PlantSerializer.SerializeOne(plant, "basic")
}
}
Field Selection
func (this *PlantJsonView) serializeWithFields(plant *mdl.Plant, fields []string) (map[string]interface{}, error) {
if len(fields) == 0 {
return this.PlantSerializer.SerializeOne(plant, "show")
}
// Create custom serialization based on requested fields
result := make(map[string]interface{})
for _, field := range fields {
switch field {
case "id":
result["id"] = plant.Id
case "name":
result["name"] = plant.Name
case "species":
result["species"] = plant.Species
case "size":
result["size"] = plant.Size
case "created_at":
result["created_at"] = plant.CreatedAt.Format(time.RFC3339)
}
}
return result, nil
}
Integration with Controllers
JSON Views are typically used in API controller methods:
func (gc *GardenController) ApiList(c *gin.Context) {
input := &maes.ListGardensMaeIn{}
// Bind query parameters
if err := gc.ListGardensBinder.Bind(c.Request.Context(), input, c); err != nil {
c.JSON(400, gc.ErrorJsonView.ValidationError(err))
return
}
// Execute business logic
output, err := gc.ListGardensMae.Execute(c.Request.Context(), input)
if err != nil {
c.JSON(500, gc.ErrorJsonView.ServerError(err))
return
}
// Serialize response
response, err := gc.ListGardensJsonView.H(output)
if err != nil {
c.JSON(500, gc.ErrorJsonView.SerializationError(err))
return
}
c.JSON(200, response)
}
Error Handling
Serialization Error Recovery
func (this *PlantJsonView) H(out *maes.ShowPlantMaeOut) (map[string]interface{}, error) {
plant, err := this.PlantSerializer.SerializeOne(out.Plant, "show")
if err != nil {
// Fallback to basic serialization
plant = map[string]interface{}{
"id": out.Plant.Id,
"name": out.Plant.Name,
"error": "Serialization incomplete",
}
}
return map[string]interface{}{
"plant": plant,
}, nil
}
Partial Failure Handling
func (this *ListPlantsJsonView) H(out *maes.ListPlantsMaeOut) (map[string]interface{}, error) {
var plants []map[string]interface{}
var errors []string
for _, plant := range out.Plants {
plantData, err := this.PlantSerializer.SerializeOne(plant, "list")
if err != nil {
errors = append(errors, fmt.Sprintf("Failed to serialize plant %s: %v", plant.Id, err))
// Include minimal data
plantData = map[string]interface{}{
"id": plant.Id,
"name": plant.Name,
"error": "Serialization failed",
}
}
plants = append(plants, plantData)
}
response := map[string]interface{}{
"plants": plants,
}
if len(errors) > 0 {
response["warnings"] = errors
}
return response, nil
}
API Versioning
Version-Specific Responses
func (this *PlantJsonView) H(out *maes.ShowPlantMaeOut) (map[string]interface{}, error) {
version := this.getAPIVersion(out.Context)
switch version {
case "v1":
return this.serializeV1(out.Plant)
case "v2":
return this.serializeV2(out.Plant)
default:
return this.serializeLatest(out.Plant)
}
}
func (this *PlantJsonView) serializeV1(plant *mdl.Plant) (map[string]interface{}, error) {
return map[string]interface{}{
"id": plant.Id,
"name": plant.Name,
"species": plant.Species,
}, nil
}
func (this *PlantJsonView) serializeV2(plant *mdl.Plant) (map[string]interface{}, error) {
return map[string]interface{}{
"id": plant.Id,
"name": plant.Name,
"species": plant.Species,
"size": plant.Size,
"perennial": plant.Perennial,
"created_at": plant.CreatedAt.Format(time.RFC3339),
}, nil
}
Performance Optimization
Lazy Loading
func (this *ListPlantsJsonView) H(out *maes.ListPlantsMaeOut) (map[string]interface{}, error) {
// Serialize basic plant data first
plants := make([]map[string]interface{}, len(out.Plants))
for i, plant := range out.Plants {
plants[i] = map[string]interface{}{
"id": plant.Id,
"name": plant.Name,
}
}
response := map[string]interface{}{
"plants": plants,
}
// Add detailed data if requested
if out.Context.IncludeDetails {
for i, plant := range out.Plants {
detailed, err := this.PlantSerializer.SerializeOne(plant, "detailed")
if err == nil {
plants[i] = detailed
}
}
}
return response, nil
}
Testing JSON Views
func TestShowPlantJsonView_H(t *testing.T) {
view := setupShowPlantJsonView()
output := &maes.ShowPlantMaeOut{
Plant: &mdl.Plant{
Id: "plant123",
Name: "Tomato",
Species: "Solanum lycopersicum",
},
}
response, err := view.H(output)
assert.NoError(t, err)
assert.Equal(t, "plant123", response["plant"].(map[string]interface{})["id"])
assert.Equal(t, "Tomato", response["plant"].(map[string]interface{})["name"])
}
func TestListPlantsJsonView_ErrorHandling(t *testing.T) {
view := setupListPlantsJsonView()
output := &maes.ListPlantsMaeOut{
Plants: []*mdl.Plant{
{Id: "plant1", Name: "Valid Plant"},
{Id: "plant2", Name: ""}, // This might cause serialization issues
},
}
response, err := view.H(output)
assert.NoError(t, err)
assert.Len(t, response["plants"], 2)
}
Best Practices
Consistency
- Use consistent response structures across all endpoints
- Maintain consistent field naming conventions
- Apply consistent error response formats
Performance
- Avoid N+1 queries in serialization
- Use appropriate serialization contexts
- Implement caching for expensive serializations
Maintainability
- Keep views focused on serialization logic
- Use serializers for complex data transformation
- Handle errors gracefully with fallbacks
API Design
- Follow RESTful response conventions
- Support field selection when appropriate
- Provide clear error messages and codes
JSON Views provide clean, consistent API responses while maintaining flexibility and performance for various API consumption patterns.