110 Custom Model Mapping: Connecting Types to Your Own Structs
110 Custom Model Mapping: Connecting Types to Your Own Structs
When building backend applications or large-scale enterprise systems, we often run into the need to perform data mapping. It’s also common for the data model from an external source (such as a response from another service, a database, or a third-party API) to not directly match the model or struct we use within our application. Custom model mapping is an elegant solution for this need: it’s how we connect “foreign” data types into structs of our own design, following our own rules.
In this article, I’ll take a deep dive into the “custom model mapping” technique, covering everything from the general concept to its practical implementation with concrete code examples, case simulations, and a flowchart to map out sound mapping logic. The main focus is the Go (Golang) language, but the pattern is actually very universal and applies to almost every statically typed language and statically typed OOP language.
Why Do We Need Custom Model Mapping?
The following scenarios are surely familiar:
- The external API has fields that are strange or irrelevant
- Field names differ between the external response and the internal domain
- A field is nullable in the API, but must be non-null in the domain
- Logical transformation: a status code of “1/0” becomes “active/inactive”
If we process the data directly across every layer of the application, the codebase becomes “fragile”—brittle, bug-prone, and hard to maintain. The better practice is to map the data into an internal model as early as possible, designed to fit our needs, so that all subsequent logic operates on this internal model.
Comparison Table: External Model vs. Internal Model
Let’s take a concrete example. Suppose we have an external service with the following response (in JSON):
| External Field | Type | Sample Value | Notes |
|---|---|---|---|
user_id | int | 2024 | Same as the user id |
name | str | “Andi Online” | User’s name |
stat | int | 1 | 1 = active, 0 = inactive, may be null |
is_admin | bool | false | True/false, optional |
Meanwhile, our internal struct is designed as follows:
| Internal Field | Type | Notes |
|---|---|---|
ID | int | User ID |
FullName | string | User’s full name |
Status | string | “active” or “inactive” |
IsSuperAdmin | bool | True only if admin |
Struct Models: External vs. Internal
1// Response from the external API
2type ExternalUser struct {
3 UserID int `json:"user_id"`
4 Name string `json:"name"`
5 Stat *int `json:"stat"` // Can be null
6 IsAdmin *bool `json:"is_admin"` // Can be null
7}
8
9// Our application's internal struct
10type User struct {
11 ID int
12 FullName string
13 Status string // "active" or "inactive"
14 IsSuperAdmin bool
15}Mapping Flow: Data Flow Diagram
Let’s visualize the type mapping process as a flowchart using mermaid:
flowchart TD
A[Ambil data dari API eksternal] --> B{Parsing JSON ke ExternalUser}
B --> C{Transform}
C -->|Stat==1| D[Status="active"]
C -->|Stat==0|null| E[Status="inactive"]
C --> F{IsAdmin==true}
F -->|ya| G[IsSuperAdmin=true]
F -->|tidak| H[IsSuperAdmin=false]
D & G --> I[Bentuk struct User]
E & H --> I
I --> J[Simpan/Proses User di aplikasi]
This flow ensures that “dirty” external data is immediately cleaned up by the time it enters the application’s internal model.
Custom Mapping: How to Implement It
We want to build a MapExternalUserToUser function that can be customized according to business rules:
1func MapExternalUserToUser(ext ExternalUser) User {
2 status := "inactive"
3 if ext.Stat != nil && *ext.Stat == 1 {
4 status = "active"
5 }
6
7 isSuperAdmin := false
8 if ext.IsAdmin != nil && *ext.IsAdmin {
9 isSuperAdmin = true
10 }
11
12 return User{
13 ID: ext.UserID,
14 FullName: ext.Name,
15 Status: status,
16 IsSuperAdmin: isSuperAdmin,
17 }
18}A brief explanation:
- Safely handles the nullable (
nil)Statvalue so it stays safe (defaulting to “inactive”) - Translates fields with different names (for example,
user_idtoID,nametoFullName) - Applies default logic for
IsSuperAdminwhen it’s nil
Usage Simulation: Code & Testing
Imagine we receive data from an external endpoint like this:
1{
2 "user_id": 2024,
3 "name": "Andi Online",
4 "stat": 1,
5 "is_admin": null
6}Example code for consuming and mapping it:
1package main
2
3import (
4 "encoding/json"
5 "fmt"
6)
7
8func main() {
9 // Simulating the external JSON response
10 data := []byte(`{"user_id":2024, "name":"Andi Online", "stat":1, "is_admin":null}`)
11 var extUser ExternalUser
12 json.Unmarshal(data, &extUser)
13
14 mappedUser := MapExternalUserToUser(extUser)
15 fmt.Printf("%+v\n", mappedUser)
16 // Output: {ID:2024 FullName:Andi Online Status:active IsSuperAdmin:false}
17}Unit test for the mapping implementation:
1func TestMapExternalUserToUser(t *testing.T) {
2 statVal := 1
3 adminVal := false
4 ext := ExternalUser{
5 UserID: 1,
6 Name: "Ujang",
7 Stat: &statVal,
8 IsAdmin: &adminVal,
9 }
10 u := MapExternalUserToUser(ext)
11 if u.Status != "active" || u.IsSuperAdmin {
12 t.Errorf("Mapping failed: %+v", u)
13 }
14}Patterns & Best Practices
- Don’t map throughout your code. Provide a single mapping layer; only the internal struct should pass through the service/business layer.
- Always cover null/unexpected cases. The example above uses pointers and default values.
- Use unit tests to handle edge cases in the mapping.
- If the rules are likely to change, split the mapping function per context, and avoid the god function.
Scaling: Mapping Lots of Data (Bulk Mapping)
If we frequently receive data in the form of a slice/array, use a helper:
1func MapExternalUsers(exts []ExternalUser) []User {
2 var users []User
3 for _, e := range exts {
4 users = append(users, MapExternalUserToUser(e))
5 }
6 return users
7}Mapping Libraries: Use Them or Not?
There are automatic mapping libraries (for Go, for example: https://github.com/mitchellh/mapstructure; for Java: MapStruct). But be careful: custom mapping often involves domain logic, so manual mapping remains the primary solution when the rules are complex.
Case Study: Error Handling During Mapping
Check for “bad data” conditions from the external source, and return an error if it isn’t mappable.
1func MapExternalUserToUserV2(ext ExternalUser) (User, error) {
2 if ext.UserID == 0 {
3 return User{}, fmt.Errorf("user_id is required")
4 }
5 // continue mapping as usual...
6 ...
7}Conclusion
Custom model mapping, from an external type to your own struct, is a fundamental pattern in software engineering. Decoupling a model like this frees your application from the “legacy burden” of external data formats, keeping the codebase clean, logical, and easy to maintain. Provide a mapping function as early as possible, invest time in testing, and handle edge cases carefully. In the long run, a model like this will surely save on maintenance costs compared to slapping data wherever it lands throughout your code.
Do you already have a “Custom Model Mapper” in the application you’re working on today?