choo

choo is a python3 library providing a uniform interface for public transport APIs.

Contents:

Getting Started

The Choo Data Model

Before we start using choo, you have to understand its underlying Models.

Stop
A Stop is a group of Platforms (e.g. Berlin Hbf).
Platform
A Platform is a place where rides stop (e.g. Gleis 7). It belongs to one Stop.
Ride
A Ride is the journey of a bus/train/etc. It only happens once. Even the “same” ride on the next day is handled as another Ride.
Line
A Line is a name for a group of Rides (e.g. Bus Line 495). Every Ride belongs to one Line.
Trip
A Trip is a connection between two Stops (or AbstractLocations to be precise, see below) with rides, interchanges and/or footpaths in between.

The models Stop, Adress and POI (Point of Interest) are all subclasses of Location which describes a stand-alone Location with City and Name

Also, Location and Platform are subclasses of AbstractLocation which describes anything that has a static position.

Those models are called Searchables because you can search for them with choo. You can...

  • provide an instance of them. choo will try to retrieve it and return a more complete version of it to you
  • use their Request submodel to specify what you are looking. choo will use the Result submodel to give you the search results.

Some other Models that are part of choo but can not be searched for:

Way
A path between two AbstractLocation objects.
TimeAndPlace
A time and place object describes the time, stop and platform and coordinates where a ride meets a stop.
RideSegment
In most cases, we are only interested in a part of a Ride – from where you enter the train/bus/etc. to where you leave it. That part is called a RideSegment – it consists of a Ride and the start and end point of the segment.
RealtimeTime
Points in time are always given as a RealtimeTime object, which consists of a datetime and a delay (if known).
Coordinates
Each AbstractLocation may have Coordinates. They consist of latitude and longitude.
WayType
Each Way has a waytype. A WayType has one of the values walk, bike, car or taxi
LineType

Each Line has a linetype. A Linetype has one of the values (empty string), train, train.local, train.longdistance, train.longdistance.highspeed, urban, metro, tram, bus, bus.regional, bus.city, bus.express, suspended, ship, dialable, or other.

An empty string means that it can be anyone of the other linetypes, The linetype bus means that it could be any of the bus-subtypes. The reason for this is that not all networks differentiate between some subtyes (e.g. bus types). See the network reference for which linetypes it may output.

For more information, see Model Reference.

Command Line Interface

usage: choo [-h] [--pretty] network query

positional arguments:
  network     network to use, e.g. vrr
  query       any Searchable or Searchable.Request as JSON

optional arguments:
  -h, --help  show this help message and exit
  --pretty    pretty-print output JSON

To get startet, let’s look up Essen Hauptbahnhof in the VRR network using the command line interface.

To do so, we first have to describe Essen Hauptbahnhof as a Stop:

["Stop", {
  "name": "Essen Hauptbahnhof"
}]

Now we pass this data to the API. We use the network vrr. --pretty means that the resulting JSON should be pretty printed.

$ choo --pretty vrr '["Stop", {"name": "Essen Hauptbahnhof"}]'

We get the following result:

[
  "Stop",
  {
    "id": 20009289,
    "source": "vrr",
    "coords": [
      51.451137,
      7.012941
    ],
    "country": "de",
    "city": "Essen",
    "name": "Hauptbahnhof",
    "full_name": "Essen Hbf",
    "ifopt": "de:5113:9289",
    "rides": {  },
    "lines": {  }
  }
]

As you can see, the API returned a Stop with more information.

The stop now is defined by it’s correct country, city, name and full_name attribute. Also, we have its coordinates now. source contains the name of the network that gave us this data. id is the ID of the Stop in this network.

The rides and lines attributes were shortened in this example but will give you Ride.Results and Line.Results if the API provides this information. If not, you can still use a Ride.Request oder Line.Request to request it explicitely.

For more information about the JSON format, see Model Reference and Model Serialization.

For more information about how to query information, see Network API.

Python Interface

Let’s see how you would access this via the Python interface.

from choo.models import Stop
from choo.networks.de import vrr

essen = Stop(name='Essen Hauptbahnhof')
essen = vrr.query(essen)

We created the Stop, got the network and used the generic .query() function of the VRR api wich gave us the same result as above.

print(essen.city)  # Essen
print(essen.name)  # Hauptbahnhof
print(essen.full_name)  # Essen Hbf

# iterates through all lines
for line in essen.lines:
    print(line.shortname)  # RB40 and similar

# iterates through all rides
for ridesegment in essen.rides:
    ride = ridesegment.ride

    print(ride.number)  # train number or similar
    print(ride.line.shortname)  # 106 or similar

    # all Ride attributes can also accessed using the RideSegment
    print(ridesegment.number)  # same as ride.number

    # iterate through all stops of the RideSegment
    for timeandplace in ridesegment:
        if timeandplace is not None:  # this is not a gap
            if timeandplace.departure is not None:  # we now the departure
                print(timeandplace.departure.time)  # planned time as datetime.datetime
                print(timeandplace.departure.delay)  # expceted delay as datetime.datetimeplanned time as datetime.datetime
                print(timeandplace.departure.is_live)  # shortcut for delay is not None
                print(timeandplace.departure.livetime)  # expceted time if real time information is available, otherwise planned time
            print(timeandplace.stop.name) # Hauptbahnhof or similar

    # iterate through all stops of the Ride
    for timeandplace in ridesegment.ride:
        # same as above, but without boundaries

    # you can also slice a ride or ride segment to get another ride segment
    newsegment = ridesegment.ride[1:]

For more information, see Model Reference.

HTTP API

usage: choo-server [-h] [--host HOST] [--port PORT]

optional arguments:
  -h, --help   show this help message and exit
  --host HOST  set address to listen on (default: 0.0.0.0)
  --port PORT  set tcp port (default: random unused port)

Just start it and open it in your browser to see the API.

How to search for a Trip

Just query the Request-submodel of Trip, like explained above. Simple example:

["Trip.Request", {
    "origin:" ["Stop", {
        "name": "Essen Hauptbahnhof"
    }],
    "destination:" ["Stop", {
        "name": "Dortmund Hauptbahnhof"
    }]
}]
Trip.Request(origin=Stop(name='Essen Hauptbahnhof'),
             destination=Stop(name='Dortmund Hauptbahnhof'))

Network Reference

class API

The base class for all other APIs.

query(obj)

Pass a Searchable and it will return a Searchable from the API or None/null if it could not be found. Pass a Searchable.Request and you will get a corresponding Searchable.Results.

class EFA

Public transport API used world wide by many network operators.

Supported Networks

VRR – Verkehrsverbund Rhein Ruhr (de.vrr EFA)
ifopt available. Also supports all of NRW and some parts of Germany.
VRN – Verkehrsverbund Rhein-Neckar (de.vrn EFA)
ifopt available. Also supports some parts of Germany and Europe.

Model Reference

Note

Every attribute can be None when no data is available, unless noted otherwise.

Base Classes

All choo models are based upon one of the following three base classes that build on top of each other.

class Serializable

All models are Serializables. See Model Serialization for more information.

validate()

Checks if all attributes have valid values. Raises an exception if the object is not valid. This method is also called by serialize().

serialize()

Serializes this object in a JSON-encodable format. This is a shortcut for Serializable.serialize(serializable).

Return type:the serialized object
classmethod serialize(obj)

Serializes the given instance of this model in a JSON-encodable format, using typed serialization if the given object is an instance of a submodel.

>>> stop.serialize()
{…}
>>> Stop.serialize(stop) == stop.serialize()
True
>>> Location.serialize(stop)
['Stop', {…}]
>>> Location.serialize(stop)[1] == stop.serialize()
True
Parameters:obj – A serialized representation of a choo object
Return type:the serialized object
classmethod unserialize(data)

Unserializes an object of this model or one of its child classes from a JSON-encodable format. Always use the same model for unserialization as you used for serialization.

Parameters:data – A serialized representation of an object of this model or one of its submodels.
Return type:the unserialized object
class Searchable

An Serializable that can be searched for. You can pass it to the API and to get its complete information.

All subclasses have a Request and a Results subclass. You can pass the and instance of the Request subclass to the API to get search results in a Results subclass.

class Request(Serializable)

A request template for this Model with lots of possible search criteria that can be set.

class Searchable.Results(Serializable)

A list (of Request-Results) of instances of this object. Those lists can have match scores.

You can just iterate over this object to get its contents.

scored(obj)

Returns an iterator over tuples of (results, score).

filter(request)

Remove all objects that do not match the given Request.

filtered(request)

Returns a new Results instance with all objects that do not match the given Request removed.

Note

For serialization, the list of results is stored in the property results as a list. Each element of this list is a two-element list containing the serialized result and the match score.

Please note that the serialized result is typed serialized if the Model has submodels (e.g. Location, which has Stop etc…)

class Collectable

A Searchable that can be collected. It has an ID and it really exists and is not some kind of data construct.

source

Source Network of this object. All APIs set this attribute, but it is not mandatory for input.

id

ID of this object (as str) in the source network.

Main Models

Submodels of Collectable.

class AbstractLocation

Base class for everything that has a fixed position.

coords

The Coordinates of this location.

class Request

Submodel of Searchable.Request.

class AbstractLocation.Results

Submodel of Searchable.Results.

class Ride(line=None, number=None)

A ride is implemented as a list of TimeAndPlace objects.

Although a Ride is iterable, most of the time not all stops of the rides are known and the list of known stations can change. This makes the use of integer indices impossible. To avoid this problem, dynamic indices are used for a Ride.

If you iterate over a Ride each item you get is None or a TimeAndPlace object. Each item that is None stands for n missing stations. It can also mean that the TimeAndPlace before and after the item are in fact the same. To get rid of all None items, pass an incomplete ride to a network API.

You can use integer indices to get, set or delete single TimeAndPlace objects which is usefull if you want the first (0) or last (-1). But, as explained above, these integer indices may point to another item when the Ride changes or becomes more complete.

If you iterate over ride.items() you get (RideStopPointer, TimeAndPlace) tuples. When used as an indice, a Ride.StopPointer used as an indice will always point to the same TimeAndPlace object.

You can slice a Ride (using integer indices or :py:class RideStopPointer`) which will get you a RideSegment that will always have the correct boundaries. Slicing with no start or no end point is also supported.

Caution

Slicing a Ride is inclusive! For example, slicing from element 2 to element 5 results in a RideSegment containing 4 elements in total!

line

Not None. The Line of this Ride.

number

The number (train number or similar) of this Ride as a string.

canceled

A boolean indicating whether this ride has been canceled.

bike_friendly

A boolean indicating whether this is a bike-friendly vehicle.

items()

A (RideStopPointer, TimeAndPlace) iterator as explained above.

append(item)

Append a TimeAndPlace object.

prepend(item)

Prepend a TimeAndPlace object.

insert(position, item)

Insert a TimeAndPlace as the new position position.

Attention

The following attributes are dynamic and can not be set.

path

Get the geographic path of the ride as a list of Coordinates.

Falls back to just directly connecting the platform or stop coordinates if no other information is available. If some information is still missing, its value is None.

is_complete

True if the TimeAndPlace list is complete and there are no Nones in the list, otherwise False.

class StopPointer

See above. Immutable. Do not use this class directly. You can cast it to int.

Note

For serialization, pointers are not used. The property stops is created containing with each item being either a serialized TimeAndPlace object or None.

The property path is created containing a dictionary containing paths between consecutive ride stops with the index of the origin stop as keys.

class Ride.Request

Submodel of Searchable.Request.

class Ride.Results

Submodel of Searchable.Results.

class Line(linetype=None)

A group of Rides (e.g. Bus Line 495). Every Ride belongs to one Line.

linetype

Not None. The LineType of this Line.

product

The product name, for example InterCity, Hamburg-Köln-Express or Niederflurbus.

name

Not None The long name of the Line, for example Rhein-Haardt-Express RE2.

shortname

Not None The short name of the Line, for example RE2.

route

The route description.

first_stop

The first Stop of this Line. Rides may start at a later station.

last_stop

The last Stop of this Line. Rides may end at a earlier station.

network

The name of the network this Line natively belongs to.

operator

The name of the company that operates this line.

class Request

Submodel of Searchable.Request.

class Line.Results

Submodel of Searchable.Results.

Locations

Submodels of AbstractLocation.

class Platform(stop, name=None, full_name=None)

An AbstractLocation where rides stop (e.g. Gleis 7). It belongs to one Stop.

ifopt

The globally unique ID of this Platform according to Identification of Fixed Objects in Public Transport supported by some APIs.

stop

Not None. The Stop this platform belongs to.

name

The name of this Platform (e.g. 7 or 2b).

full_name

The full name of this Platform (e.g. Bussteig 7 or Gleis 2b)

class Request

Submodel of AbstractLocation.Request.

class Platform.Results

Submodel of AbstractLocation.Results.

class Location(country=None, city=None, name=None)

An AbstractLocation that is named and not a sublocation like a Platform.

country

The country of this location as a two-letter country code.

city

The name of the city this location is located in.

name

The name of this location. If the city attribute is None this it may also included in the name.

near_stops

Other stops near this one as a Stop.Results, if available. You can always search for Stops near an AbstractLocation directly using AbstractLocation.Request.

class Request

Submodel of AbstractLocation.Request.

name

A search string for the name of the Location.

city

City of the Location.

class Location.Results

Submodel of AbstractLocation.Results.

class Stop(country=None, city=None, name=None)

A Location describing a stop, for example: Düsseldorf Hbf.

ifopt

The globally unique ID of this Stop according to Identification of Fixed Objects in Public Transport supported by some APIs.

uic

The is the international train station id by the International Union of Railways.

full_name

The full name of this Stop. Can be just the city and the name, but does’nt have to.

lines

The Lines that are available at this stop as a Line.Results object, if available. You can always search for Lines at a Stop using Line.Request.

rides

The next rides at this stop as a Ride.Results object, if available. You can always search for Rides at a Stop using Ride.Request.

class Request

Submodel of Location.Request.

class Stop.Results

Submodel of Location.Results.

class Address(country=None, city=None, name=None)

A Location describing an address. The name attribute contains the address in one string, but more detailed attributes may be available:

street

The name of the street.

number

The house number as a string.

class Request

Submodel of Location.Request.

class Address.Results

Submodel of Location.Results.

class POI(country=None, city=None, name=None)

A Location describing a Point of Interest.

class Request

Submodel of Location.Request.

class POI.Results

Submodel of Location.Results.

Trips

Submodel of Searchable.

class Trip

A connection from a AbstractLocation to another AbstractLocation.

It consists of a list of RideSegment and Way objects. Just iterate over it to get its elements.

time

The fetching time of this object as a datetime object. This is relevant to know how up to date the contained real time data (delays, cancellation, platform changes, etc.) is. All APIs set this attribute, but it is not mandatory for input.

tickets

TicketList of available tickets for this trip.

Attention

The following attributes are dynamic and can not be set.

origin

The start AbstractLocation of this trip.

destination

The end AbstractLocation of this trip.

departure

The departure at the first AbstractLocation of this trip as RealtimeTime. (If there are leading Way objects they need to have the duration attribute set in order for this to work)

arrival

The arrival at the last AbstractLocation of this trip as RealtimeTime. (If there are trailing Way objects they need to have the duration attribute set in order for this to work)

linetypes

The line types that occur in this trip as LineTypes.

wayonly

A boolean indicating whether this Trip only consists of Way objects.

changes

The number of changes in this trip (number of RideSegments minus one with a minimum of zero)

bike_friendly

False if at least one Ride that is part of this trip is not bike friendly. True if all of them are. None if there is no bike friendly information for all rides but those that have the information are bike friendly.

Note

For serialization, the property parts is created containing the list of typed serialized trip parts.

class Request

Submodel of Searchable.Request.

origin

Not None. The start AbstractLocation of the trip.

destination

Not None. The end AbstractLocation of the trip.

departure

The minimum departure time as RealtimeTime or datetime.datetime.

If both times are None the behaviour is as if you would have set the departure time to the current time right before sending the request. (Default: None)

arrival

The latest allowed arrival as RealtimeTime or datetime.datetime. (Default: None)

linetypes

The line types that are allowed as LineTypes. (Default: all)

max_changes

The maximum number of changes allowed or None for no limit. (Default: None)

with_bike

Whether a bike should be taken along. (Default: False)

wheelchair

Whether to allow only vehicles that support wheelchairs. (Default: False)

low_floor_only

Whether to allow only low floor vehicles. (Default: False)

allow_solid_stairs

Whether to allow solid stairs. (Default: True)

allow_escalators

Whether to allow escalators. (Default: True)

allow_elevators

Whether to allow elevators. (Default: True)

waytype_origin

Waytype at the beginning of the trip. (Default: walk)

waytype_via

Waytype at changes or ways during the trip. (Default: walk)

waytype_destination

Waytype at the end of the trip. (Default: walk)

wayduration_origin

Maximum duration of a way at the beginning of the trip as a datetime.timedelta. (Default: 10 minutes)

wayduration_via

Maximum duration of changes of ways during the trip as a datetime.timedelta. (Default: 10 minutes)

wayduration_destination

Maximum duration of a way at the end of the trip as a datetime.timedelta. (Default: 10 minutes)

class Trip.Results

Submodel of Searchable.Results.

origin

Not None. The start AbstractLocation of the trip.

destination

Not None. The end AbstractLocation of the trip.

Trip parts

Submodels of Serializable.

class RideSegment
This class created by slicing :py:class:`Ride` objects.

Integer indices are not too useful in this class, either, although you can for example still use 0 and -1 to get the first or last RideStopPointer of this segment.

This model is usable in the same way as a Ride. Slicing it will return another RideSegment for the same Ride.

Caution

Slicing a RideSegment is inclusive! For example, slicing from element 2 to element 5 results in a RideSegment containing 4 elements in total!

ride

Not None. The Ride that this object is a segment of.

items()

A (RideStopPointer, TimeAndPlace) iterator over this segment.

All attributes of the Ride are also directly accessible through a RideSegment.

Attention

The following attributes are dynamic and can not be set.

path

Get the geographic path of the ride segment as a list of Coordinates.

Falls back to just directly connecting the platform or stop coordinates if no other information is available. If some information is still missing, its value is None.

is_complete

True if the TimeAndPlace list of this Segment is complete.

origin

The first Stop of this segment. Shortcut for segment[0].stop.

destination

The last Stop of this segment. Shortcut for segment[-1].stop.

departure

The departure at the first Stop of this segment as RealtimeTime. Shortcut for segment[0].departure.

arrival

The arrival at the last Stop of this segment as RealtimeTime. Shortcut for segment[-1].arrival.

Note

For serialization, the boundaries are given as integer indexes as properties origin and destination. Each one can be missing if the boundary is not set. (e.g. ride[5:])

Dont forget that Ride slicing is inclusive (see above)!

class Way(origin: Location, destination: Location, distance: int=None)

Individual transport (walk, bike, taxi…) with no schedule. Used for example to get from a Address to a Stop and for changes but also for trips that are faster by foot.

origin

The start point Location.

destination

The end point Location.

distance

The distance in meters as int.

duration

The expected duration as datetime.timedelta.

path

The path as a list of Coordinates.

events

Events on the way (e.g. taking escalators upwards) as a (ordered) list of WayEvent.

Other Models

Submodels of Serializable.

class TimeAndPlace(platform, arrival=None, departure=None)

Time and place of a Ride stopping at a Platform.

platform

Not None. The Platform.

arrival

The arrival time of the Ride as RealtimeTime.

departure

The departure time of the Ride as RealtimeTime.

passthrough

A boolean indicating whether the ride does not actualle stop at this Stop but pass through it.

class RealtimeTime(time, delay=None)

A point in time with optional real time data.

Parameters:
  • time – The originally planned time as a datetime.datetime object.
  • delay – The (expected) delay as a datetime.timedelta object if known.
time

Not None. The originally planned time as a datetime.datetime object.

delay

The (expected) delay as a datetime.timedelta object or None. Please note that a zero delay is not the same as None. None stands for absence of real time information.

Attention

The following attributes are dynamic and can not be set.

is_live

True if there is real time data available. Shortcut for delay is not None

livetime

The (expected) actual time as a datetime.datetime object if real time data is available, otherwise the originally planned time.

class TicketList(all_types: bool=True)

A list of tickets.

currency

Not None. The name or abbreviation of the currency.

level_name

How a level is named at this network.

single

Not None. The single ticket as TicketData.

bike

The single ticket as TicketData.

other

Not None. Other available tickets as a dictionary with the name of the tickets as keys and TicketData objects as values.

Data types

Submodels of Serializable.

class TicketData(authority=None, level=None, price=None, price_child=None)

Information about a ticket.

authority

The name of the authority selling this ticket.

level

The level of this ticket, e.g. A or something similar, depending on the network

price

Not None. The price of this ticket as float.

price_child

The children’s price for this ticket if this ticket is not a ticket for children only but has a different price for children.

class LineType(name)

Each Line has a line type. A line type has one of the values (empty string), train, train.local, train.longdistance, train.longdistance.highspeed, urban, metro, tram, bus, bus.regional, bus.city, bus.express, bus.longdistance, suspended, ship, dialable, or other.

An empty string means that it can be anyone of the other linetypes, The linetype bus means that it could be any of the bus-subtypes. The reason for this is that not all networks differentiate between some subtyes (e.g. bus types). See the network reference for which linetypes it may output.

All identical linetypes are the same instance:

>>> LineType('bus') is LineType('bus')
True

To compare against a linetype, use the in operator. Be aware that this operator is not transitive!

>>> linetype = LineType('bus.express')
>>> linetype in LineType('bus')
True
>>> LineType('bus') in linetype
False

>>> LineType('bus') in LineType('')
True
>>> LineType('') in LineType('bus')
False
>>> LineType('bus') in LineType('bus')
True

You can cast a LineType to string if needed:

>>> str(LineType('train.local'))
'train.local'

Note

The serialized representation of this model is its string representation.

class LineTypes(include=('', ), exclude=())

A selector for LineType object. It is defined as a list of included line types and a list of excluded linetypes. By default, all line types are included.

>>> LineType('bus') in LineTypes()
True

>>> LineType('bus') in LineTypes(exclude=('bus', ))
False
>>> LineType('bus.express') in LineTypes(exclude=('bus', ))
False
>>> LineType('bus') in LineTypes(exclude=('bus.express', ))
True
>>> LineType('bus.express') in LineTypes(exclude=('bus.express', ))
False

>>> LineType('train') in LineTypes(include=('bus', ), exclude=('bus.express', ))
False
>>> LineType('bus') in LineTypes(include=('bus', ), exclude=('bus.express', ))
True
>>> LineType('bus.express') in LineTypes(include=('bus', ), exclude=('bus.express', ))
False

You can modify the selector using the following methods:

include(*linetypes)
Parameters:linetypes – one or more line types as string or LineType

Make sure that the given line types and all of their subtypes are matched by the selector.

exclude(*linetypes)
Parameters:linetypes – one or more line types as string or LineType

Make sure that the given line types and all of their subtypes are not matched by the selector.

Note

For serialization, the properties included and excluded are created, each one containing a list of line types.

class WayType(name)

Each Way has a line type. A Linetype has one of the values walk, bike, car, taxi.

All identical way types are the same instance:

>>> WayType('walk') is WayType('walk')
True

You can cast a WayType to string if needed:

>>> str(WayType('walk'))
'walk'

Note

The serialized representation of this model is its string representation.

class WayEvent(name, direction)

A way Way events one of the names stairs, escalator or elevator and one of the directions up or down.

All identical way types are the same instance:

>>> WayType('escalator', 'down') is WayType('escalator', 'down')
True

Attention

The following attributes are dynamic and can not be set.

name

Not None. stairs, escalator or elevator

direction

Not None. up or down

Note

The serialized representation of this model is a (name, direction) tuple.

class Coordinates(lat, lon)

A geographic coordinate.

lat

latitude as float

longitude

longitude as float

Note

The serialized representation of this model is a (lat, lon) tuple.

Model Serialization / JSON

All choo models can be (un)serialized to/from a JSON-encodable format.

In Python

from models import Stop, Location, Serializable
# Create a Stop
stop = Stop(city='Essen', name='Hauptbahnhof')

# simple serialization
serialized = stop.serialize()
stop = Stop.unserialize(serialized)

# possibly typed serialization
# this will save the model type if the given object is a submodel
serialized = Location.serialize(stop)
stop = Location.unserialize(serialized)

# this will be a simple serialization because the given Model is not a submodel
serialized = Stop.serialize(stop)
stop = Stop.unserialize(serialized)

# always typed serialization
serialized = Serializable.serialize(stop)
stop = Serializable.unserialize(serialized)

How it works

Caution

Some models have a non-dictionary representation or some additional attributes in their dictionary representation. See the Model Reference for more information.

All public attributes that are not dynamic and not None are put into a dictionary. All values are serialized.

datetime values are respresented as a string in YYYY-MM-DD HH:II:SS format.
timedelta values are respresented as a the total number of seconds as int.
{
    "_ids": {"vrr": 20009289},
    "country": "de",
    "city": "Essen",
    "name": "Hauptbahnhof",
    "coords": [51.451139, 7.012937]
}

If the serialization is typed, the output is a two element list with name of the model and the constructed dictionary (or other respresentation).

["Stop", {
    "_ids": {"vrr": 20009289},
    "country": "de",
    "city": "Essen",
    "name": "Hauptbahnhof",
    "coords": [51.451139, 7.012937]
}]

Indices and tables