Skip to content

Encoding

Amir Iranmanesh edited this page Aug 11, 2026 · 1 revision

Encoding: JSON, text and SQL

ptime.Time implements the standard encoding interfaces, so it drops into structs and database columns without a wrapper type.

Interface Methods
json.Marshaler, json.Unmarshaler MarshalJSON, UnmarshalJSON
encoding.TextMarshaler, encoding.TextUnmarshaler MarshalText, UnmarshalText
driver.Valuer Value
sql.Scanner Scan

JSON

The wire format is RFC3339Nano: 1404-01-01T00:00:00+03:30, with the fractional second trimmed of trailing zeros and omitted when zero.

type Event struct {
    Name string     `json:"name"`
    At   ptime.Time `json:"at"`
}

b, _ := json.Marshal(Event{
    Name: "نوروز",
    At:   ptime.Date(1404, ptime.Farvardin, 1, 0, 0, 0, 0, ptime.Iran()),
})
// {"name":"نوروز","at":"1404-01-01T00:00:00+03:30"}

var back Event
json.Unmarshal(b, &back)

The zero Time is null, in both directions:

json.Marshal(ptime.Time{})            // null
json.Unmarshal([]byte("null"), &pt)   // pt.IsZero() == true

Because TextUnmarshaler is implemented, a ptime.Time also works as a map key and in any encoder that speaks text — YAML, TOML, encoding/xml, URL query binding libraries.

A different JSON shape

If you need something else on the wire, wrap the type — this is the same pattern you would use for time.Time:

type PersianDate ptime.Time

func (d PersianDate) MarshalJSON() ([]byte, error) {
    return json.Marshal(ptime.Time(d).Format(ptime.DateOnly))
}

func (d *PersianDate) UnmarshalJSON(b []byte) error {
    var s string
    if err := json.Unmarshal(b, &s); err != nil {
        return err
    }

    pt, err := ptime.ParseInLocation(ptime.DateOnly, s, ptime.Iran())
    if err != nil {
        return err
    }

    *d = PersianDate(pt)

    return nil
}

SQL

Value hands the driver a Gregorian time.Time, so the column is an ordinary SQL timestamp. That keeps ORDER BY, range queries, indexes and every other database date function working, and it keeps the data readable to non-Go consumers.

at := ptime.Date(1404, ptime.Farvardin, 1, 0, 0, 0, 0, ptime.Iran())

db.Exec("INSERT INTO events (name, created_at) VALUES ($1, $2)", "نوروز", at)

var got ptime.Time
db.QueryRow("SELECT created_at FROM events WHERE id = $1", id).Scan(&got)

Scan accepts:

Source Handling
nil the zero Time
time.Time converted through New
string, []byte parsed as a Gregorian timestamp

Text is read as Gregorian, not Persian, because that is what Value writes and the two are indistinguishable on the wire — 1394-07-02 is a valid date in both calendars. The layouts tried, in order:

2006-01-02T15:04:05.999999999Z07:00
2006-01-02 15:04:05.999999999 -0700 MST
2006-01-02 15:04:05.999999999-07:00
2006-01-02 15:04:05.999999999
2006-01-02 15:04:05
2006-01-02

Anything else fails with a *ParseError. A nil driver.Value is written for the zero Time, so it round trips through a nullable column.

Storing the Persian date as text

If you deliberately want the Persian form in the column — for a report table someone reads directly, say — store the formatted string and parse it back yourself:

db.Exec("INSERT INTO reports (day) VALUES ($1)", at.Format(ptime.DateOnly))

var day string
db.QueryRow("SELECT day FROM reports WHERE id = $1", id).Scan(&day)
at, err := ptime.ParseInLocation(ptime.DateOnly, day, ptime.Iran())

Keep in mind you lose the database's date arithmetic when you do this.

Clone this wiki locally