Declaring input and output
Declare what a service accepts and returns - parsed before handle runs, validated and documented in OpenAPI.
A service can declare what it accepts and what it returns, and there are two forms to do it in - a list of field names, or a data model. Either way, a declaration means the input is parsed before handle runs, required fields are enforced, optional ones read as None when not sent, and the declaration feeds the OpenAPI documentation generated for the service.
Declarations themselves are optional - without one, self.request.input holds the incoming message as it arrived, as the requests and responses page shows.
Names in a tuple
The simplest declaration is a tuple of field names. Each name is its own string in the tuple - one string is always one field, never a comma-separated list of them - and a single field needs no tuple at all:
# -*- coding: utf-8 -*-
# Zato
from zato.server.service import Service
class GetProfile(Service):
input = 'customer_id', 'tier'
output = 'display_name', 'is_active'
def handle(self):
customer_id = self.request.input.customer_id
tier = self.request.input.tier
self.response.payload.display_name = f'Customer {customer_id}, tier {tier}'
self.response.payload.is_active = True
Optional fields - the dash prefix
A leading dash makes a field optional. The minus sign is part of the declaration only, never of the name itself - -priority is read as priority:
# -*- coding: utf-8 -*-
# Zato
from zato.server.service import Service
class GetProfile(Service):
input = 'customer_id', '-priority'
def handle(self):
customer_id = self.request.input.customer_id
# None when the caller did not send it
priority = self.request.input.priority
if priority is None:
self.logger.info(f'No priority given for {customer_id}')
else:
self.logger.info(f'{customer_id} has priority {priority}')
A field without a dash is required. A request missing one is rejected before handle runs, with an error naming the field, e.g. Missing required input element: customer_id. The dash is the whole mechanism - there is no separate is_required attribute to set anywhere.
What a name says about its type
The name of a field decides what type it arrives as:
- Names starting with
by_,has_,is_,may_,needs_orshould_arrive as booleans - Names ending in
_countor_timeoutarrive as integers - Every other name arrives as a string
# -*- coding: utf-8 -*-
# Zato
from zato.server.service import Service
class UpdateSubscription(Service):
input = 'customer_id', 'is_active', 'retry_count'
def handle(self):
# A bool, from the is_ prefix
is_active = self.request.input.is_active
# An int, from the _count suffix, ready for arithmetic
retry_count = self.request.input.retry_count
next_retry = retry_count + 1
self.logger.info(f'Active: {is_active}, next retry: {next_retry}')
When a field needs a type these conventions do not cover - an integer that is not a count, a date, a nested structure - declare a model instead. There is no force_type or other per-field override for names alone - a type the conventions cannot express is what models are for.
Declaring output
Output takes the same form, and declaring it makes the response include the declared names only. Assigning a name that was not declared raises an error at the very line that assigns it, so typos never reach the wire:
# -*- coding: utf-8 -*-
# Zato
from zato.server.service import Service
class GetBalance(Service):
output = 'account_id', 'balance'
def handle(self):
self.response.payload.account_id = 'A-1001'
self.response.payload.balance = 250
A dict assigned as a whole is filtered to the declared names too, so an internal dict passes through without leaking fields. The full treatment of building responses - free-form payloads, list responses, nested shapes - is on the requests and responses page.
Data models
The second form is a data model - a dataclass whose fields have real types, defaults and structure. self.request.input is then an instance of the model:
# -*- coding: utf-8 -*-
# stdlib
from dataclasses import dataclass
# Zato
from zato.common.typing_ import optional
from zato.server.service import Model, Service
@dataclass(init=False)
class CreateOrderRequest(Model):
customer_id: str
quantity: int
notes: optional[str] = None
@dataclass(init=False)
class CreateOrderResponse(Model):
order_id: str
is_confirmed: bool
class CreateOrder(Service):
input = CreateOrderRequest
output = CreateOrderResponse
def handle(self):
request = self.request.input # This is a CreateOrderRequest instance
response = CreateOrderResponse()
response.order_id = f'order-{request.customer_id}-{request.quantity}'
response.is_confirmed = True
self.response.payload = response
With a model, quantity above is an int because the field says so, not because of its name, and a request missing it is rejected with a response that names the field, so the caller knows exactly what to fix. Nested models, lists of models and everything else models can do is on the data models page.
Which form to use
Names in a tuple are enough when the fields are strings, booleans and counts that the name conventions cover, which is most lookup and status services. A model is the right form when fields have types of their own, defaults, or structure - and once a request has more than a handful of fields, a model also gives the IDE completion and type checking that plain names cannot.
The two forms mix freely across services - one service can declare names while another in the same project declares models.