Fungsi c.BodyParser() di Go Fiber secara default mendekompresi payload JSON secara permisif. Jika klien mengirimkan atribut yang tidak didefinisikan di struct target—misalnya karena salah ketik nama field—data tersebut diabaikan secara diam-diam (silent drop). Backend menerima nilai kosong atau zero-value tanpa menghasilkan pesan error apa pun.

Risiko Silent Drop terhadap Integritas Kontrak API

Perilaku default parser membawa risiko fatal pada arsitektur sistem berbasis kontrak (schema-first atau API-contract first):

  • Kerusakan Nilai Bisnis: Klien mengirimkan {"acount_id": "12345"} (salah ketik). Struct Go dengan field AccountID tetap terisi string kosong (""). Proses transaksi berjalan tanpa identitas rekening yang valid.
  • Bypass Logika Mutasi: Pada endpoint update (PATCH/PUT), field yang diabaikan menyebabkan sistem menganggap klien tidak berniat memperbarui atribut tersebut, padahal terjadi ketidakcocokan versi SDK klien dan server.
  • Masalah Audit dan Keamanan: Payload yang membawa properti tambahan ilegal tidak terdokumentasi dan lolos masuk ke lapisan logika aplikasi.

Custom Strict JSON Parser Menggunakan DisallowUnknownFields

Untuk memvalidasi field yang tidak dikenali, gunakan json.NewDecoder dari paket standar Go encoding/json yang digabungkan dengan method DisallowUnknownFields(). Baca byte buffer mentah via c.Body() menggunakan bytes.NewReader.

Pendekatan ini memisahkan parsing ketat dari binder default Fiber:

package parser

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"strings"

	"github.com/gofiber/fiber/v2"
)

func ParseStrictJSON(c *fiber.Ctx, out interface{}) error {
	body := c.Body()
	if len(body) == 0 {
		return errors.New("body payload cannot be empty")
	}

	reader := bytes.NewReader(body)
	decoder := json.NewDecoder(reader)
	decoder.DisallowUnknownFields()

	if err := decoder.Decode(out); err != nil {
		var syntaxErr *json.SyntaxError
		var unmarshalTypeErr *json.UnmarshalTypeError

		switch {
		case errors.As(err, &syntaxErr):
			return fmt.Errorf("malformed json at offset %d: %w", syntaxErr.Offset, err)

		case errors.As(err, &unmarshalTypeErr):
			return fmt.Errorf("invalid type for field '%s' at offset %d", unmarshalTypeErr.Field, unmarshalTypeErr.Offset)

		case strings.HasPrefix(err.Error(), "json: unknown field "):
			fieldName := strings.TrimPrefix(err.Error(), "json: unknown field ")
			return fmt.Errorf("unmapped field rejected: %s", fieldName)

		case errors.Is(err, io.EOF):
			return errors.New("request body is empty")

		default:
			return err
		}
	}

	// Pastikan tidak ada data JSON ganda di belakang payload
	if decoder.More() {
		return errors.New("body must only contain a single JSON object")
	}

	return nil
}

Integrasi dengan go-playground/validator/v10

Parsing skema yang ketat hanya memvalidasi bentuk representasi JSON. Untuk memverifikasi nilai wajib (required) dan format bisnis, gabungkan custom parser dengan validator/v10.

package main

import (
	"net/http"

	"github.com/go-playground/validator/v10"
	"github.com/gofiber/fiber/v2"
)

var validate = validator.New()

type CreateUserRequest struct {
	Username string `json:"username" validate:"required,min=4,max=32"`
	Email    string `json:"email" validate:"required,email"`
	Age      int    `json:"age" validate:"gte=0,lte=130"`
}

func UserHandler(c *fiber.Ctx) error {
	var req CreateUserRequest

	// 1. Strict JSON Decode
	if err := ParseStrictJSON(c, &req); err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"error":  "Bad Request",
			"detail": err.Error(),
		})
	}

	// 2. Struct Validation
	if err := validate.Struct(req); err != nil {
		return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{
			"error":  "Validation Failed",
			"detail": err.Error(),
		})
	}

	return c.Status(fiber.StatusCreated).JSON(fiber.Map{
		"status": "success",
		"data":   req,
	})
}

Pengujian Otomatis Menggunakan app.Test

Verifikasi penolakan unknown field menggunakan test harness bawaan Fiber (app.Test) tanpa perlu menjalankan port jaringan eksternal.

package main

import (
	"bytes"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/gofiber/fiber/v2"
)

func setupTestApp() *fiber.App {
	app := fiber.New()
	app.Post("/users", UserHandler)
	return app
}

func TestStrictJSONParser_RejectsUnknownField(t *testing.T) {
	app := setupTestApp()

	// Payload membawa atribut asing 'role' yang tidak ada pada struct
	payload := []byte(`{
		"username": "johndoe",
		"email": "[email protected]",
		"age": 28,
		"role": "admin"
	}`)

	req := httptest.NewRequest(http.MethodPost, "/users", bytes.NewReader(payload))
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req, -1)
	if err != nil {
		t.Fatalf("failed to run test request: %v", err)
	}

	if resp.StatusCode != fiber.StatusBadRequest {
		t.Errorf("expected status %d, got %d", fiber.StatusBadRequest, resp.StatusCode)
	}
}

func TestStrictJSONParser_AcceptsValidPayload(t *testing.T) {
	app := setupTestApp()

	payload := []byte(`{
		"username": "johndoe",
		"email": "[email protected]",
		"age": 28
	}`)

	req := httptest.NewRequest(http.MethodPost, "/users", bytes.NewReader(payload))
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req, -1)
	if err != nil {
		t.Fatalf("failed to run test request: %v", err)
	}

	if resp.StatusCode != fiber.StatusCreated {
		t.Errorf("expected status %d, got %d", fiber.StatusCreated, resp.StatusCode)
	}
}

Trade-off dan Pertimbangan Performa

Menerapkan DisallowUnknownFields memiliki konsekuensi yang perlu diperhitungkan sebelum diterapkan ke seluruh endpoint:

  • Evolusi Schema API: Klien yang memperbarui versi JSON sebelum backend diperbarui akan mengalami kegagalan request. Backward compatibility menjadi lebih kaku.
  • Alokasi Memori: Penggunaan c.Body() membaca seluruh buffer ke memori RAM, berbeda dengan streaming langsung dari soket TCP. Untuk payload berukuran besar (di atas beberapa megabyte), batasi limit request body menggunakan middleware limiter.