Skip to content
Santekno.com | Tech Tutorials and Trends
EN
📖 0%
18 Oct 2025 · 6 min read ·Article 110 / 125
Go

110 Custom Model Mapping: Connecting Types to Your Own Structs

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

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 FieldTypeSample ValueNotes
user_idint2024Same as the user id
namestr“Andi Online”User’s name
statint11 = active, 0 = inactive, may be null
is_adminboolfalseTrue/false, optional

Meanwhile, our internal struct is designed as follows:

Internal FieldTypeNotes
IDintUser ID
FullNamestringUser’s full name
Statusstring“active” or “inactive”
IsSuperAdminboolTrue only if admin

Struct Models: External vs. Internal

go
 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:

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:

go
 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) Stat value so it stays safe (defaulting to “inactive”)
  • Translates fields with different names (for example, user_id to ID, name to FullName)
  • Applies default logic for IsSuperAdmin when it’s nil

Usage Simulation: Code & Testing

Imagine we receive data from an external endpoint like this:

json
1{
2    "user_id": 2024,
3    "name": "Andi Online",
4    "stat": 1,
5    "is_admin": null
6}

Example code for consuming and mapping it:

go
 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:

go
 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:

go
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.

go
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?

Related Articles

💬 Comments