Your Go struct kept one item. The XML had three.
The WSDL said choice with maxOccurs="unbounded". The generator gave you single pointer fields, encoding/xml overwrote each element with the next, and your program silently kept only the last one. That is hooklift/gowsdl#264, and lather's regression test reproduces it exactly before proving the fix.
lather is a WSDL 1.1 toolkit for Go that gets collections right: occurrence bounds survive parsing, and repeated elements - including members of a repeating choice - generate as slices. Nothing is dropped on the floor.
$ lather -p billing 'https://vendor.example/SoapService.asmx?WSDL'
wrote billing/billing.goc := countryinfo.NewCountryInfoServiceSoapTypeClient()
resp, err := c.CapitalCity(ctx, &countryinfo.CapitalCity{SCountryISOCode: "IL"})
// resp.CapitalCityResult == "Jerusalem" - verified live, see belowCLI:
go install github.com/ofershap/lather/cmd/lather@latestLibrary:
go get github.com/ofershap/latherdef, err := wsdl.Parse(r) // WSDL 1.1 -> occurrence-aware model
src, err := gen.Generate(def, gen.Options{Package: "billing"})
err = client.Call(ctx, action, req, &resp) // or use the generated typed client- Parses WSDL 1.1 into an occurrence-aware model: types, messages, portTypes, bindings (SOAP 1.1 and 1.2), services and endpoints.
DOCTYPE/DTD is refused outright. - Generates compilable Go: structs with
xmltags, a typed client per portType with the SOAPAction and endpoint taken from the binding, gofmt-clean output. - Collections are correct.
maxOccurs> 1 orunboundedgenerates slices - including every member of an enclosing repeatingchoice, the exact shape that loses data in hooklift/gowsdl#264. A regression test proves the round trip keeps all items. - Namespace collisions are explicit. Two types with the same local name in different namespaces get a deterministic suffix instead of one silently redefining the other.
- Message roots are namespace-qualified when the schema says
elementFormDefault="qualified", so strict servers accept the request. - Unknown or unresolved type references degrade to
XSDAny, which preserves the raw XML instead of failing or fabricating a type. - Runtime client: SOAP 1.1/1.2 envelopes, fault decoding into a
Faulterror, WS-Security UsernameToken with timestamp, raw header injection (WS-Addressing, vendor headers), response size cap, and refusal of DTD'd responses (XXE hardening). Standard library only - no dependencies.
- WSDL 2.0,
rpc/encoded, and multi-part RPC messages are out of scope. xs:import/xs:includeare not resolved - lather reads a single self-contained WSDL document (imports are recorded but not fetched). Most.asmx?WSDLand vendor WSDLs are self-contained.- No WS-Security signing or encryption - UsernameToken only. The
Headersfield accepts signed fragments produced elsewhere. - Generated clients are synchronous over
net/http; retries, auth refresh and connection pooling belong to yourhttp.Client.
Every claim below is reproducible from the test suite:
- gowsdl#264 round trip:
gentests build the exact failing shape (repeatingchoice), unmarshal three items, and assert all three survive. The same shape generated by gowsdl keeps one. - Generated code compiles: tests write the output into a temp module and run
go buildon it. - Real WSDL corpus: the generated code for the public InterSystems crcind SOAP.Demo (28 structs + client) and oorsprong CountryInfoService (57 structs + client) compiles cleanly.
- Live call: an opt-in test (
LATHER_LIVE=1 go test ./soap/ -run TestLive) generates a request, calls the real CountryInfoService endpoint, and getsJerusalemback forIL. CI never touches the network. - Security behavior is tested: DTD in a WSDL is rejected, DTD in a response is rejected, faults decode, SOAP 1.2 content types are correct.
- Parse the WSDL into a model that keeps occurrence bounds (
min/max) on every particle instead of flattening them away. - Plan each field: repeating compositors make their members slices, self-references become pointers, unknown references become raw-XML
XSDAny. - Emit structs,
XSDDateTimehelpers and a typed client per portType, then run the result throughgo/format.
How do I call a SOAP service from Go?
Point lather at the service's WSDL URL, then use the generated typed client: construct it, call the method, handle the typed response or thesoap.Fault error. No reflection and no hand-built envelopes.What is the best WSDL-to-Go generator?
lather is built for the cases that bite in production: repeatingchoice collections (gowsdl#264), colliding type names across namespaces, and strict servers that require namespace-qualified message roots. If your WSDL is WSDL 2.0, uses rpc/encoded, or splits types across imported documents, lather is not the right tool today.Does lather support SOAP 1.2?
Yes. The runtime sends SOAP 1.2 envelopes with the correctapplication/soap+xml content type when the binding says 1.2, and decodes SOAP 1.2 fault shapes.How does lather handle WS-Security?
It sends a UsernameToken with a created timestamp, and theHeaders field injects any raw XML (WS-Addressing, vendor tokens, signatures computed by your own code). Signing and encryption are not implemented.Is it safe to parse untrusted WSDLs or responses?
DTD/DOCTYPE is refused in both directions, responses are size-capped (32 MiB by default), and the toolkit is standard-library only, so its vulnerability surface is Go's own.Issues and PRs welcome - see CONTRIBUTING.md. Please report security issues privately per SECURITY.md.
More projects: gitshow · real-browser-mcp · cursor-usage-tracker · mcp-server-markdown · readme-builder · grok-bot-skills · mashov-parent-brief · gailwind
README built with README Builder
