Skip to content

UpdateDomainRequestBody is an exclusive union, so UpdateDomain silently drops fields #6

Description

@patramsey

PATCH /core/v1/domains/{domainName} accepts any combination of
autorenewEnabled, privacyEnabled, and locked. The spec combines them with
anyOf, which means at least one, so a body carrying two or three is valid.

The SDK models that anyOf as an exclusive union. UpdateDomainRequestBody
holds three pointers and a private typ, and MarshalJSON returns on the first
non-nil variant:

func (u UpdateDomainRequestBody) MarshalJSON() ([]byte, error) {
	if u.typ == "UpdateDomainRequestBodyAutorenewEnabled" || u.UpdateDomainRequestBodyAutorenewEnabled != nil {
		return json.Marshal(u.UpdateDomainRequestBodyAutorenewEnabled)
	}
	if u.typ == "UpdateDomainRequestBodyPrivacyEnabled" || u.UpdateDomainRequestBodyPrivacyEnabled != nil {
		return json.Marshal(u.UpdateDomainRequestBodyPrivacyEnabled)
	}
	if u.typ == "UpdateDomainRequestBodyLocked" || u.UpdateDomainRequestBodyLocked != nil {
		return json.Marshal(u.UpdateDomainRequestBodyLocked)
	}
	return nil, fmt.Errorf("type %T does not include a non-empty union type", u)
}

Setting more than one field silently discards all but the first. There is no
error and no warning; the request succeeds.

Version: v1.33.3 · Go: 1.26.6

Reproduction

package main

import (
	"encoding/json"
	"fmt"

	coreapigo "github.com/namedotcom/core-api-go"
)

func main() {
	body := &coreapigo.UpdateDomainRequestBody{
		UpdateDomainRequestBodyAutorenewEnabled: &coreapigo.UpdateDomainRequestBodyAutorenewEnabled{AutorenewEnabled: true},
		UpdateDomainRequestBodyPrivacyEnabled:   &coreapigo.UpdateDomainRequestBodyPrivacyEnabled{PrivacyEnabled: false},
		UpdateDomainRequestBodyLocked:           &coreapigo.UpdateDomainRequestBodyLocked{Locked: false},
	}
	b, err := json.Marshal(body)
	fmt.Printf("all three set -> %s (err=%v)\n", b, err)
}

Actual

all three set -> {"autorenewEnabled":true} (err=<nil>)

Expected

all three set -> {"autorenewEnabled":true,"locked":false,"privacyEnabled":false} (err=<nil>)

Impact

locked is the transfer lock. A caller that sets it alongside another field —
which the API permits and which is the natural shape of a read-modify-write
update — gets a 200 and no change to the lock. The next thing that happens is
an outbound transfer that should have been blocked.

privacyEnabled is billable, and can be dropped the same way.

The failure mode is the one that is hardest to catch: the code reads correctly,
compiles, and passes any test that does not assert the bytes on the wire.

Suggested fix

Model the body as one object with three optional fields, matching the spec.
Over sibling properties, anyOf means "at least one of these", not "exactly one
of these" — the latter is oneOf. If the "at least one" constraint should be
enforced, validating it at the call site preserves the ability to send several.

It is worth checking whether other anyOf request bodies in this SDK are
modelled the same way. This one surfaced only because the operation happens to
be one where sending several fields together is the normal usage.

Note

.fernignore exempts only .fern/replay.lock, .fern/replay.yml, and
.gitattributes, so domains.go is regenerated. The durable fix is likely in
the API description or in the generator's anyOf handling rather than in a
patch to the Go source.

Workaround, for anyone who finds this first

Pass a nil Body and supply the fields through option.WithBodyProperties,
which the SDK marshals verbatim:

_, err := client.Domains.UpdateDomain(ctx,
	&coreapigo.UpdateDomainRequest{DomainName: domain},
	option.WithBodyProperties(map[string]any{
		"autorenewEnabled": autorenew,
		"privacyEnabled":   privacy,
		"locked":           locked,
	}))

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions