WSDL to OpenAPI — how each SOAP operation becomes a REST endpoint

· The Vizroute team

The rules Vizroute uses to turn a WSDL into an OpenAPI design — verbs, paths, identifiers, status codes and faults — and the cases it flags for a person to decide.

A WSDL and an OpenAPI file describe the same thing in two shapes. SOAP is a list of operations, each a verb with an envelope: GetAccountBalance, CloseAccount, TransferFunds. REST is a set of resources, each with a handful of methods: GET /accounts/{accountId}/balance. Converting one to the other is mostly a naming problem, and most of it can be done by reading the names carefully.

Vizroute does that reading when you open a WSDL. Paste it, upload a .wsdl or .xml file, or give its URL to the API Map, and you get a proposed REST design drawn as a map. This post sets out the rules it follows, so you know which parts of the proposal to trust and which to check.

What you get from a real WSDL

Here is the bank service that ships as a sample, with every operation and the endpoint proposed for it:

SOAP operationMethodProposed path
GetAccountBalanceGET/accounts/{accountId}/balance
GetAccountTransactionsGET/accounts/{accountId}/transactions
FindAccountsByCustomerGET/customers/{customerId}/accounts
OpenAccountPOST/accounts
UpdateAccountAddressPUT/accounts/{accountId}/address
CloseAccountDELETE/accounts/{accountId}
TransferFundsPOST/funds/transfer
ValidateIbanPOST/ibans/validate

Each of those rows came from one of the rules below.

The method comes from the first word

The operation name is split into words, and the first word picks the HTTP method:

First wordMethod
Get, Find, Fetch, List, Search, Lookup, Check, CountGET
Create, Add, Insert, Register, Submit, Open, IssuePOST
Update, Set, Modify, Change, Save, Replace, AssignPUT
Patch, Adjust, AmendPATCH
Delete, Remove, Cancel, Close, Revoke, TerminateDELETE

That is why CloseAccount becomes a DELETE. Closing an account is not always a deletion in your domain, which is exactly the kind of call the proposal leaves visible for you to change.

Some verbs are not reads or writes of a resource at all. Validate, Transfer, Calculate, Approve, Reject, Refund and some fifty others are treated as commands. A command stays a POST, and the verb becomes the last segment of the path: TransferFunds is POST /funds/transfer. Forcing a transfer onto PUT or PATCH would describe it worse than SOAP did.

When no verb matches, as with SayHello, there is a fallback. If every input is a plain value, there are at most four of them, and there is a result, the operation is proposed as a GET. Otherwise it is a POST, the safe default for an action. Either way the operation is flagged for review, with the reason written down.

The path comes from the rest of the name and the input fields

The words after the verb name the resource. The last word is made plural for the collection, so OpenAccount becomes POST /accounts.

An identifier comes from the request, not the name. If an input field is named after the resource plus Id, Number, Code, Ref or Key, it becomes a path parameter. In GetAccountBalance, the request has an accountId field, so the account is the resource, {accountId} goes in the path, and the word left over, Balance, becomes a sub-resource: GET /accounts/{accountId}/balance.

A "By", "For" or "Of" clause names a filter, or a parent. FindAccountsByCustomer takes a customerId, and since that identifies a customer, the accounts are nested under it: GET /customers/{customerId}/accounts. If the clause matches an ordinary field instead of an identifier, the field becomes a query parameter.

Plain input fields on a GET or DELETE become query parameters. Structured criteria do not fit in a query string, so a GET whose request carries nested objects or lists is proposed as POST /…/search with the criteria as the body. That one is always flagged: it works, but it is a compromise you may prefer to flatten.

Status codes and errors

The success status follows from what the operation returns:

  • A creation that returns something answers 201 Created. One that returns nothing answers 202 Accepted.
  • An operation whose response element is empty answers 204 No Content.
  • A one-way operation, with no output message at all, answers 202 Accepted.
  • If the response wrapper holds a single field named like Result or Return, that field is returned directly instead of the wrapper.

SOAP faults become HTTP errors by their names:

Fault name containsStatus
NotFound, Unknown, Missing, NoSuch404
Invalid, Validation, Malformed, Required400
Unauthorized, Credential, Token, Expired401
Forbidden, Permission, Denied403
Conflict, Duplicate, AlreadyExists, Locked409
Insufficient, Limit, Quota, Funds422
Timeout, Unavailable, Busy503
anything else500

An InsufficientFundsFault on TransferFunds is therefore a 422, which is what a REST client would expect for a request that is well formed but cannot be carried out.

What it will not decide for you

Every proposal carries a confidence figure and a list of reasons. The confidence drops when the rules had to guess:

  • no recognised verb;
  • several identifiers in one request, where it has to pick one for the path;
  • a DELETE or a PUT with no identifier, which would act on a whole collection;
  • a search squeezed into a POST.

Those are the rows to read first. The rest usually need nothing more than a glance.

Some things a name cannot tell you at all. Whether "close" means delete or a status change, whether two operations are really the same resource, or how your teams name things already: that is a review, not a rule. The proposal gets you to that review with the mechanical part done.

Try it on your own WSDL

Open the API Map, choose Upload, and pick your .wsdl file. The map shows the proposed resources, and the Raw Spec tab shows the OpenAPI document behind them. If your WSDL imports its schemas from separate XSD files, the SOAP Workbench (where your site has it switched on) takes the WSDL and its XSDs together. Everything is read in your browser, and nothing is uploaded.

The user guide lists every format the API Map reads.