Contribute

Edit this documentation

Translate the documentation or open the complete project on your computer.

Nothing is published until you choose to submit your work.

Data Models

Bind complex data structures between Go and JavaScript

Data Model Bindings

Wails automatically generates JavaScript/TypeScript classes from Go structs, providing full type safety when passing complex data between backend and frontend. Write Go structs, generate bindings, and get fully-typed frontend models complete with constructors, type annotations, and JSDoc comments.

Quick Start

Go struct:

go
type User struct {
    ID        int       `json:"id"`
    Name      string    `json:"name"`
    Email     string    `json:"email"`
    CreatedAt time.Time `json:"createdAt"`
}

func (s *UserService) GetUser(id int) (*User, error) {
    // Return user
}

Generate:

bash
wails3 generate bindings

JavaScript:

javascript
import { GetUser } from './bindings/changeme/userservice'
import { User } from './bindings/changeme/models'

const user = await GetUser(1)
console.log(user.Name)  // Type-safe!

That’s it! Full type safety across the bridge.

Defining Models

Basic Struct

go
type Person struct {
    Name string
    Age  int
}

Generated JavaScript:

javascript
export class Person {
    /** @type {string} */
    Name = ""
    
    /** @type {number} */
    Age = 0
    
    constructor(source = {}) {
        Object.assign(this, source)
    }
    
    static createFrom(source = {}) {
        return new Person(source)
    }
}

With JSON Tags

go
type User struct {
    ID        int       `json:"id"`
    Name      string    `json:"name"`
    Email     string    `json:"email"`
    CreatedAt time.Time `json:"createdAt"`
}

Generated JavaScript:

javascript
export class User {
    /** @type {number} */
    id = 0
    
    /** @type {string} */
    name = ""
    
    /** @type {string} */
    email = ""
    
    /** @type {Date} */
    createdAt = new Date()
    
    constructor(source = {}) {
        Object.assign(this, source)
    }
}

JSON tags control field names in JavaScript.

Custom JSON Marshalling

Bound method results are marshalled with Go’s standard encoding/json package. Be aware that MarshalJSON methods declared on a pointer receiver are only called when the value being marshalled is addressable. This means a custom marshaler can be called for elements of a returned slice but not for a struct returned by value:

go
func (n *Nullable[T]) MarshalJSON() ([]byte, error) {
    if n.Valid {
        return json.Marshal(n.V)
    }
    return []byte("null"), nil
}

func (s *UserService) GetUser() User {
    return User{} // pointer receiver may not be called
}

func (s *UserService) GetUsers() []User {
    return []User{{}} // pointer receiver is called for elements
}

To ensure consistent output, either define MarshalJSON on a value receiver when the type can be copied safely, or return a pointer to the struct (for example, *User). This is a behaviour of Go’s encoding/json, rather than a difference in Wails’ binding generation.

With Comments

go
// User represents an application user
type User struct {
    // Unique identifier
    ID int `json:"id"`
    
    // Full name of the user
    Name string `json:"name"`
    
    // Email address (must be unique)
    Email string `json:"email"`
}

Generated JavaScript:

javascript
/**
 * User represents an application user
 */
export class User {
    /**
     * Unique identifier
     * @type {number}
     */
    id = 0
    
    /**
     * Full name of the user
     * @type {string}
     */
    name = ""
    
    /**
     * Email address (must be unique)
     * @type {string}
     */
    email = ""
}

Comments become JSDoc! Your IDE shows them.

Nested Structs

go
type Address struct {
    Street  string `json:"street"`
    City    string `json:"city"`
    Country string `json:"country"`
}

type User struct {
    ID      int     `json:"id"`
    Name    string  `json:"name"`
    Address Address `json:"address"`
}

Generated JavaScript:

javascript
export class Address {
    /** @type {string} */
    street = ""
    
    /** @type {string} */
    city = ""
    
    /** @type {string} */
    country = ""
}

export class User {
    /** @type {number} */
    id = 0
    
    /** @type {string} */
    name = ""
    
    /** @type {Address} */
    address = new Address()
}

Usage:

javascript
const user = new User({
    id: 1,
    name: "Alice",
    address: new Address({
        street: "123 Main St",
        city: "Springfield",
        country: "USA"
    })
})

Arrays and Slices

go
type Team struct {
    Name    string   `json:"name"`
    Members []string `json:"members"`
}

Generated JavaScript:

javascript
export class Team {
    /** @type {string} */
    name = ""
    
    /** @type {string[]} */
    members = []
}

Usage:

javascript
const team = new Team({
    name: "Engineering",
    members: ["Alice", "Bob", "Charlie"]
})

Maps

go
type Config struct {
    Settings map[string]string `json:"settings"`
}

Generated JavaScript:

javascript
export class Config {
    /** @type {Record<string, string>} */
    settings = {}
}

Usage:

javascript
const config = new Config({
    settings: {
        theme: "dark",
        language: "en"
    }
})

Type Mapping

Primitive Types

Go Type JavaScript/TypeScript
string string
bool boolean
int, int8, int16, int32, int64 number
uint, uint8, uint16, uint32, uint64 number
float32, float64 number
byte number
rune number

Complex Types

Go Type JavaScript/TypeScript Notes
[]T T[] -
[N]T T[] -
map[string]T { [_: string]: T } string-keyed map
map[K]V { [_ in K]?: V } non-string K is rendered as a mapped type, not a JS Map
[]byte string base64-encoded
struct class / interface with fields
time.Time any serialized as RFC3339Nano string in runtime
*T T | null pointer means nullable
any / interface{} any -
error any / Exception Exception if as return value else any

Unsupported Types

  • chan T (channels)
  • func() (functions)
  • Complex interfaces (except interface{})
  • Unexported fields (lowercase)

Using Models

Creating Instances

javascript
import { User } from './bindings/changeme/models'

// Empty instance
const user1 = new User()

// With data
const user2 = new User({
    id: 1,
    name: "Alice",
    email: "alice@example.com"
})

// From JSON string
const user3 = User.createFrom('{"id":1,"name":"Alice"}')

// From object
const user4 = User.createFrom({ id: 1, name: "Alice" })

Passing to Go

javascript
import { CreateUser } from './bindings/changeme/userservice'
import { User } from './bindings/changeme/models'

const user = new User({
    name: "Bob",
    email: "bob@example.com"
})

const created = await CreateUser(user)
console.log("Created user:", created.id)

Receiving from Go

javascript
import { GetUser } from './bindings/changeme/userservice'

const user = await GetUser(1)

// user is already a User instance
console.log(user.name)
console.log(user.email)
console.log(user.createdAt.toISOString())

Updating Models

javascript
import { GetUser, UpdateUser } from './bindings/changeme/userservice'

// Get user
const user = await GetUser(1)

// Modify
user.name = "Alice Smith"
user.email = "alice.smith@example.com"

// Save
await UpdateUser(user)

TypeScript Support

Generated TypeScript

bash
wails3 generate bindings -ts

Generated:

typescript
/**
 * User represents an application user
 */
export class User {
    /**
     * Unique identifier
     */
    id: number = 0
    
    /**
     * Full name of the user
     */
    name: string = ""
    
    /**
     * Email address (must be unique)
     */
    email: string = ""
    
    /**
     * Account creation date
     */
    createdAt: Date = new Date()
    
    constructor(source: Partial<User> = {}) {
        Object.assign(this, source)
    }
    
    static createFrom(source: string | Partial<User> = {}): User {
        const parsedSource = typeof source === 'string' 
            ? JSON.parse(source) 
            : source
        return new User(parsedSource)
    }
}

Usage in TypeScript

typescript
import { GetUser, CreateUser } from './bindings/changeme/userservice'
import { User } from './bindings/changeme/models'

async function example() {
    // Create user
    const newUser = new User({
        name: "Alice",
        email: "alice@example.com"
    })
    
    const created: User = await CreateUser(newUser)
    
    // Get user
    const user: User = await GetUser(created.id)
    
    // Type-safe access
    console.log(user.name.toUpperCase())  // ✅ string method
    console.log(user.id + 1)              // ✅ number operation
    console.log(user.createdAt.getTime()) // ✅ Date method
}

Advanced Patterns

Optional Fields

go
type User struct {
    ID       int     `json:"id"`
    Name     string  `json:"name"`
    Nickname *string `json:"nickname,omitempty"`
}

JavaScript:

javascript
const user = new User({
    id: 1,
    name: "Alice",
    nickname: "Ally"  // Optional
})

// Check if set
if (user.nickname) {
    console.log("Nickname:", user.nickname)
}

Enums

The binding generator automatically detects Go named types with constants and generates TypeScript enums or JavaScript const objects — including a $zero member for Go’s zero value and full JSDoc preservation.

go
type UserRole string

const (
    RoleAdmin UserRole = "admin"
    RoleUser  UserRole = "user"
    RoleGuest UserRole = "guest"
)

Generated TypeScript:

typescript
export enum UserRole {
    $zero = "",
    RoleAdmin = "admin",
    RoleUser = "user",
    RoleGuest = "guest",
}

Usage:

javascript
import { User, UserRole } from './bindings/changeme/models'

const admin = new User({
    name: "Admin",
    role: UserRole.RoleAdmin
})

For comprehensive coverage of string enums, integer enums, type aliases, imported package enums, and limitations, see the dedicated Enums page.

Validation

javascript
class User {
    validate() {
        if (!this.name) {
            throw new Error("Name is required")
        }
        if (!this.email.includes('@')) {
            throw new Error("Invalid email")
        }
        return true
    }
}

// Use
const user = new User({ name: "Alice", email: "alice@example.com" })
user.validate()  // ✅

const invalid = new User({ name: "", email: "invalid" })
invalid.validate()  // ❌ Throws

Serialisation

javascript
// To JSON
const json = JSON.stringify(user)

// From JSON
const user = User.createFrom(json)

// To plain object
const obj = { ...user }

// From plain object
const user2 = new User(obj)

Complete Example

Go:

go
package main

import (
    "time"
    "github.com/wailsapp/wails/v3/pkg/application"
)

type Address struct {
    Street  string `json:"street"`
    City    string `json:"city"`
    Country string `json:"country"`
}

type User struct {
    ID        int       `json:"id"`
    Name      string    `json:"name"`
    Email     string    `json:"email"`
    Address   Address   `json:"address"`
    CreatedAt time.Time `json:"createdAt"`
}

type UserService struct {
    users []User
}

func (s *UserService) GetAll() []User {
    return s.users
}

func (s *UserService) GetByID(id int) (*User, error) {
    for _, user := range s.users {
        if user.ID == id {
            return &user, nil
        }
    }
    return nil, fmt.Errorf("user %d not found", id)
}

func (s *UserService) Create(user User) User {
    user.ID = len(s.users) + 1
    user.CreatedAt = time.Now()
    s.users = append(s.users, user)
    return user
}

func (s *UserService) Update(user User) error {
    for i, u := range s.users {
        if u.ID == user.ID {
            s.users[i] = user
            return nil
        }
    }
    return fmt.Errorf("user %d not found", user.ID)
}

func main() {
    app := application.New(application.Options{
        Services: []application.Service{
            application.NewService(&UserService{}),
        },
    })
    
    app.Window.New()
    app.Run()
}

JavaScript:

javascript
import { GetAll, GetByID, Create, Update } from './bindings/changeme/userservice'
import { User, Address } from './bindings/changeme/models'

class UserManager {
    async loadUsers() {
        const users = await GetAll()
        this.renderUsers(users)
    }
    
    async createUser(name, email, address) {
        const user = new User({
            name,
            email,
            address: new Address(address)
        })
        
        try {
            const created = await Create(user)
            console.log("Created user:", created.id)
            this.loadUsers()
        } catch (error) {
            console.error("Failed to create user:", error)
        }
    }
    
    async updateUser(id, updates) {
        try {
            const user = await GetByID(id)
            Object.assign(user, updates)
            await Update(user)
            this.loadUsers()
        } catch (error) {
            console.error("Failed to update user:", error)
        }
    }
    
    renderUsers(users) {
        const list = document.getElementById('users')
        list.innerHTML = users.map(user => `
            <div class="user">
                <h3>${user.name}</h3>
                <p>${user.email}</p>
                <p>${user.address.city}, ${user.address.country}</p>
                <small>Created: ${user.createdAt.toLocaleDateString()}</small>
            </div>
        `).join('')
    }
}

const manager = new UserManager()
manager.loadUsers()

Best Practices

✅ Do

  • Use JSON tags - Control field names
  • Add comments - They become JSDoc
  • Use time.Time - Converts to Date
  • Validate on Go side - Don’t trust frontend
  • Keep models simple - Data containers only
  • Use pointers for optional - *string for nullable

❌ Don’t

  • Don’t add methods to Go structs - Keep them as data
  • Don’t use unexported fields - Won’t be bound
  • Don’t use complex interfaces - Not supported
  • Don’t forget JSON tags - Field names matter
  • Don’t nest too deeply - Keep it simple

Next Steps

Method Bindings

Learn how to bind Go methods.

Learn More →

Services

Organise code with services.

Learn More →

Best Practices

Binding design patterns.

Learn More →

Go-Frontend Bridge

Understand the bridge mechanism.

Learn More →


Questions? Ask in Discord or check the binding examples.

Edit page

Last updated: