Conseils sur l'amélioration des performances

Ce document présente quelques techniques dont vous pouvez vous servir pour améliorer les performances de votre application. Dans certains cas, nous nous basons sur des exemples d'autres API ou d'API génériques pour illustrer les idées exposées. Cependant, les mêmes concepts s'appliquent à l'API Google Wallet.

Compression avec gzip

La compression gzip est un moyen pratique et facile de réduire la bande passante requise pour chaque requête. Même si la décompression des résultats nécessite un temps CPU supplémentaire, la compression est généralement très avantageuse en matière de coûts de réseau.

Pour pouvoir recevoir une réponse encodée au format gzip, vous devez effectuer deux opérations : définir un en-tête Accept-Encoding et modifier votre user-agent afin d'y inclure la chaîne gzip. Voici un exemple d'en-têtes HTTP correctement mis en forme pour l'activation de la compression gzip :

Accept-Encoding: gzip
User-Agent: my program (gzip)

Utiliser une partie des ressources

Un autre moyen d'améliorer les performances de vos appels d'API consiste à n'envoyer et recevoir que la partie des données qui vous intéresse. Ainsi, vous évitez à votre application le transfert, l'analyse et le stockage de champs inutiles, et vos ressources (réseau, processeur, mémoire, etc.) peuvent être utilisées plus efficacement.

Il existe deux types de requêtes partielles :

  • Réponse partielle : requête dans laquelle vous spécifiez les champs à inclure dans la réponse (utilisez le paramètre de requête fields).
  • Patch : requête de mise à jour pour laquelle vous n'envoyez que les champs à modifier (utilisez le verbe HTTP PATCH).

Vous trouverez plus d'informations sur les requêtes partielles dans les sections suivantes.

Réponse partielle

Par défaut, le serveur renvoie la représentation complète d'une ressource après avoir traité les requêtes. Pour de meilleures performances, vous pouvez demander au serveur de n'envoyer que les champs dont vous avez vraiment besoin afin d'obtenir une réponse partielle.

Pour demander une réponse partielle, utilisez le paramètre de requête fields afin de spécifier les champs qui vous intéressent. Vous pouvez utiliser ce paramètre pour toute requête qui renvoie des données de réponse.

Notez que le paramètre fields n'affecte que les données de réponse. Il n'a aucune incidence sur les éventuelles données à envoyer. Pour réduire le volume de données que vous envoyez lors de la modification des ressources, utilisez une requête patch.

Exemple

L'exemple suivant illustre l'utilisation du paramètre fields avec une API générique (fictive) nommée "demo".

Requête simple : cette requête HTTP GET omet le paramètre fields et renvoie la ressource complète.

https://www.googleapis.com/demo/v1

Réponse complète : les données de la ressource complète incluent les champs suivants, en plus de plusieurs autres champs qui ont été omis pour des raisons de concision.

{
  "kind": "demo",
  ...
  "items": [
  {
    "title": "First title",
    "comment": "First comment.",
    "characteristics": {
      "length": "short",
      "accuracy": "high",
      "followers": ["Jo", "Will"],
    },
    "status": "active",
    ...
  },
  {
    "title": "Second title",
    "comment": "Second comment.",
    "characteristics": {
      "length": "long",
      "accuracy": "medium"
      "followers": [ ],
    },
    "status": "pending",
    ...
  },
  ...
  ]
}

Requête de réponse partielle : la requête suivante portant sur cette même ressource utilise le paramètre fields afin de réduire de manière significative le volume de données renvoyées.

https://www.googleapis.com/demo/v1?fields=kind,items(title,characteristics/length)

Réponse partielle : en réponse à la requête ci-dessus, le serveur ne renvoie que les informations "kind" ainsi qu'un tableau d'articles dépouillé ne comprenant que le titre HTML et les caractéristiques de longueur de chaque article.

200 OK
{
  "kind": "demo",
  "items": [{
    "title": "First title",
    "characteristics": {
      "length": "short"
    }
  }, {
    "title": "Second title",
    "characteristics": {
      "length": "long"
    }
  },
  ...
  ]
}

Notez que la réponse est un objet JSON qui ne comprend que les champs sélectionnés et leurs objets parents englobants.

La mise en forme du paramètre fields et les éléments précis renvoyés dans la réponse sont détaillés ci-dessous.

Récapitulatif de la syntaxe du paramètre "fields"

Le format de la valeur du paramètre de requête fields repose globalement sur la syntaxe XPath. La syntaxe acceptée est résumée ci-dessous, et des exemples supplémentaires sont présentés dans la section suivante.

  • Incluez une liste dont les éléments sont séparés par une virgule pour sélectionner plusieurs champs.
  • Incluez a/b pour sélectionner un champ b imbriqué dans le champ a. Incluez a/b/c pour sélectionner un champ c imbriqué dans le champ b.

    Exception : Pour les réponses de l'API qui utilisent des wrappers "data", lorsque la réponse est imbriquée dans un objet data de type data: { ... }, n'incluez pas "data" dans la spécification fields. Si vous ajoutez l'objet "data" à une spécification "fields" comme data/a/b, vous obtenez une erreur. Utilisez plutôt une spécification fields simple comme a/b.

  • Incluez un sous-sélecteur pour demander un ensemble de sous-champs spécifiques de tableaux ou d'objets en plaçant des expressions entre parenthèses : "( )".

    Par exemple, fields=items(id,author/email) ne renvoie que l'ID de l'article et l'adresse e-mail de l'auteur pour chaque article du tableau. Vous pouvez aussi spécifier un sous-champ unique, où fields=items(id) équivaut à fields=items/id.

  • Si nécessaire, utilisez des caractères génériques dans les sélections de champs.

    Par exemple, fields=items/pagemap/* sélectionne tous les objets du champ "pagemap".

Autres exemples d'utilisation du paramètre "fields"

Les exemples ci-dessous décrivent l'impact de la valeur de paramètre fields sur la réponse.

Remarque : Comme toutes les valeurs de paramètre de requête, la valeur du paramètre fields doit être encodée au format URL. Pour faciliter la lecture, les exemples de ce document omettent l'encodage.

Identifiez les champs devant être renvoyés ou effectuez des sélections de champs.
La valeur de paramètre de requête fields est une liste de champs séparés par une virgule, et chaque champ est spécifié en relation avec la racine de la réponse. Ainsi, si vous exécutez une opération list, la réponse correspond à une collection et inclut généralement un tableau de ressources. Si vous exécutez une opération qui renvoie une ressource unique, les champs sont spécifiés par rapport à cette ressource. Si le champ que vous sélectionnez est un tableau (ou figure dans un tableau), le serveur renvoie la partie sélectionnée de tous les éléments du tableau.

Exemples pour les collections :
Exemples Effet
items Renvoie tous les éléments du tableau "items", y compris tous les champs de chaque élément, mais aucun autre champ.
etag,items Renvoie le champ etag ainsi que tous les éléments du tableau "items".
items/title Ne renvoie que le champ title pour tous les éléments du tableau "items".

Chaque fois qu'un champ imbriqué est renvoyé, la réponse inclut les objets parents englobants. Les champs parents ne contiennent pas d'autres champs enfants, sauf si ceux-ci sont également sélectionnés explicitement.
context/facets/label Ne renvoie que le champ label pour tous les membres du tableau facets, qui est lui-même imbriqué sous l'objet context.
items/pagemap/*/title Pour chaque élément du tableau "items", ne renvoie que le champ title (s'il existe) de tous les objets qui sont des enfants de pagemap.

Exemples au niveau des ressources :
Exemples Effet
title Renvoie le champ title de la ressource demandée.
author/uri Renvoie le sous-champ uri de l'objet author de la ressource demandée.
links/*/href
Renvoie le champ href de tous les objets enfants de links.
Ne demandez que des parties de champs spécifiques à l'aide de sous-sélections.
Par défaut, si votre requête spécifie des champs particuliers, le serveur renvoie les objets ou les éléments de tableau dans leur intégralité. Vous pouvez spécifier une réponse n'incluant que certains sous-champs. Pour ce faire, utilisez la syntaxe de sous-sélection "( )", comme indiqué dans l'exemple ci-dessous.
Exemple Effet
items(title,author/uri) Ne renvoie que la valeur title et la valeur uri de l'auteur pour chaque élément du tableau "items".

Gérer les réponses partielles

Une fois qu'un serveur a traité une requête valide incluant le paramètre fields, il renvoie le code d'état HTTP 200 OK ainsi que les données demandées. Si le paramètre de requête fields comporte une erreur ou s'il n'est pas valide pour une autre raison, le serveur renvoie le code d'état HTTP 400 Bad Request ainsi qu'un message d'erreur indiquant à l'utilisateur la raison pour laquelle sa sélection de champs est incorrecte (par exemple, "Invalid field selection a/b").

Voici l'exemple de réponse partielle mentionné dans l'introduction ci-dessus. La requête contient le paramètre fields de manière à spécifier les champs à renvoyer.

https://www.googleapis.com/demo/v1?fields=kind,items(title,characteristics/length)

La réponse partielle se présente comme suit :

200 OK
{
  "kind": "demo",
  "items": [{
    "title": "First title",
    "characteristics": {
      "length": "short"
    }
  }, {
    "title": "Second title",
    "characteristics": {
      "length": "long"
    }
  },
  ...
  ]
}

Remarque : Dans le cas des API qui acceptent des paramètres de requête pour la pagination des données (par exemple, maxResults et nextPageToken), utilisez ces paramètres afin que la taille des résultats de chaque requête soit gérable. Cela permet de garantir l'amélioration des performances possible grâce à la réponse partielle.

Patch (mise à jour partielle)

Vous pouvez aussi éviter l'envoi de données inutiles lors de la modification des ressources. Pour n'envoyer que les données mises à jour des champs auxquels vous apportez des modifications, utilisez le verbe HTTP PATCH. La sémantique patch dans ce document est différente de l'ancienne implémentation GData de la mise à jour partielle, et elle est plus simple.

L'utilisation de patch limite les données à envoyer pour effectuer une petite mise à jour, comme illustré par le court exemple ci-dessous.

Exemple

Dans cet exemple, une simple requête patch permet de ne mettre à jour que le titre d'une ressource API "demo" générique (fictive). La ressource comporte également un commentaire, un ensemble de caractéristiques, un état et plusieurs autres champs, mais cette requête n'envoie que le champ title, puisqu'il s'agit du seul champ à modifier :

PATCH https://www.googleapis.com/demo/v1/324
Authorization: Bearer your_auth_token
Content-Type: application/json

{
  "title": "New title"
}

Réponse :

200 OK
{
  "title": "New title",
  "comment": "First comment.",
  "characteristics": {
    "length": "short",
    "accuracy": "high",
    "followers": ["Jo", "Will"],
  },
  "status": "active",
  ...
}

Le serveur renvoie un code d'état 200 OK, ainsi que la représentation complète de la ressource mise à jour. Seule la valeur du champ title a été modifiée, puisqu'il s'agit du seul champ inclus dans la requête patch.

Remarque : Vous pouvez améliorer l'efficacité de vos requêtes de mise à jour en combinant le paramètre de réponse partielle fields avec patch. Une requête patch ne permet de réduire que la taille de la requête. Toutefois, en utilisant une réponse partielle, vous pouvez également réduire la taille de la réponse. La solution pour limiter le volume de données envoyées dans les deux sens consiste donc à exécuter une requête patch comportant le paramètre fields.

Sémantique d'une requête patch

Le corps de la requête patch ne comprend que les champs de la ressource à modifier. Lorsque vous spécifiez un champ, vous devez intégrer tous les objets parents englobants (ceux-ci étant renvoyés avec une réponse partielle). Les données modifiées que vous envoyez sont fusionnées avec les données de l'objet parent, le cas échéant.

  • Ajouter : pour ajouter un champ qui n'existe pas encore, spécifiez le nouveau champ et sa valeur.
  • Modifier : pour modifier la valeur d'un champ existant, spécifiez ce champ et définissez sa nouvelle valeur.
  • Supprimer : pour supprimer un champ, spécifiez ce champ et définissez-le sur null. Exemple : "comment": null. Vous pouvez également supprimer un objet entier (s'il est modifiable) en le définissant sur null. Si vous utilisez la bibliothèque cliente des API Java, indiquez plutôt Data.NULL_STRING. Pour en savoir plus, consultez la section JSON null.

Remarque à propos des tableaux : Les requêtes patch contenant des tableaux remplacent le tableau existant par celui que vous fournissez. Il est impossible de modifier, d'ajouter ou de supprimer les éléments d'un tableau de manière fragmentaire.

Utiliser patch dans le cadre d'un cycle lecture-modification-écriture

Il peut être utile de commencer par obtenir une réponse partielle avec les données à modifier. Cette étape est particulièrement importante pour les ressources qui utilisent des ETag, car vous devez fournir la valeur ETag en cours d'utilisation dans l'en-tête HTTP If-Match afin que la ressource soit correctement mise à jour. Une fois que vous avez obtenu ces données, vous pouvez modifier les valeurs de votre choix et renvoyer la représentation partielle modifiée à l'aide d'une requête patch. L'exemple qui suit suppose que la ressource "demo" utilise des ETag :

GET https://www.googleapis.com/demo/v1/324?fields=etag,title,comment,characteristics
Authorization: Bearer your_auth_token

La réponse partielle se présente sous la forme suivante :

200 OK
{
  "etag": "ETagString"
  "title": "New title"
  "comment": "First comment.",
  "characteristics": {
    "length": "short",
    "level": "5",
    "followers": ["Jo", "Will"],
  }
}

La requête patch suivante est basée sur cette réponse. Comme indiqué ci-dessous, cette requête patch utilise également le paramètre fields afin de limiter les données renvoyées dans la réponse :

PATCH https://www.googleapis.com/demo/v1/324?fields=etag,title,comment,characteristics
Authorization: Bearer your_auth_token
Content-Type: application/json
If-Match: "ETagString"
{
  "etag": "ETagString"
  "title": "",                  /* Clear the value of the title by setting it to the empty string. */
  "comment": null,              /* Delete the comment by replacing its value with null. */
  "characteristics": {
    "length": "short",
    "level": "10",              /* Modify the level value. */
    "followers": ["Jo", "Liz"], /* Replace the followers array to delete Will and add Liz. */
    "accuracy": "high"          /* Add a new characteristic. */
  },
}

Le serveur renvoie un code d'état HTTP 200 OK, ainsi que la représentation partielle de la ressource mise à jour :

200 OK
{
  "etag": "newETagString"
  "title": "",                 /* Title is cleared; deleted comment field is missing. */
  "characteristics": {
    "length": "short",
    "level": "10",             /* Value is updated.*/
    "followers": ["Jo" "Liz"], /* New follower Liz is present; deleted Will is missing. */
    "accuracy": "high"         /* New characteristic is present. */
  }
}

Élaborer directement une requête patch

Dans certains cas, vous devez baser vos requêtes patch sur des données qu'il vous faut d'abord obtenir. Par exemple, si vous souhaitez ajouter un élément dans un tableau sans perdre aucun des éléments existants qu'il contient, vous devez d'abord obtenir ces données. De même, si une API utilise des ETags, vous devez joindre l'ancienne valeur de l'ETag à votre requête pour mettre à jour la ressource.

Remarque : Vous pouvez spécifier un en-tête HTTP "If-Match: *" pour forcer la mise à jour partielle lorsque des ETag sont utilisés. Dans ce cas, il n'est pas nécessaire de procéder à la lecture avant l'écriture.

Dans d'autres cas, vous pouvez cependant élaborer la requête patch directement, sans avoir à extraire d'abord les données existantes. Par exemple, vous pouvez facilement configurer une requête patch visant à mettre à jour la valeur d'un champ ou à en ajouter un. Voici un exemple :

PATCH https://www.googleapis.com/demo/v1/324?fields=comment,characteristics
Authorization: Bearer your_auth_token
Content-Type: application/json

{
  "comment": "A new comment",
  "characteristics": {
    "volume": "loud",
    "accuracy": null
  }
}

Avec cette requête, si le champ de commentaire possède déjà une valeur, celle-ci est remplacée par la nouvelle. Dans le cas contraire, il prend la nouvelle valeur. De même, si une caractéristique de volume est présente, sa valeur est remplacée. Dans le cas contraire, elle est créée. S'il est défini, le champ de précision est supprimé.

Traiter la réponse à une requête patch

Une fois que l'API a traité une requête patch correctement formulée, elle renvoie un code de réponse HTTP 200 OK ainsi que la représentation complète de la ressource modifiée. Si l'API utilise des ETags, le serveur met à jour les valeurs des ETags lorsqu'il réussit à traiter une requête patch, tout comme il le fait avec PUT.

La requête patch renvoie la représentation complète de la ressource, sauf si vous définissez le paramètre fields de manière à réduire le volume de données renvoyées.

Si une requête patch entraîne un nouvel état de ressource incorrect au niveau syntaxique ou sémantique, le serveur renvoie un code d'état HTTP 400 Bad Request ou 422 Unprocessable Entity, et l'état de la ressource n'est pas modifié. Par exemple, si vous tentez de supprimer la valeur d'un champ obligatoire, le serveur renvoie une erreur.

Autre notation syntaxique lorsque le verbe HTTP "PATCH" n'est pas accepté

Si votre pare-feu n'autorise pas les requêtes HTTP PATCH, créez une requête HTTP POST, puis définissez l'en-tête de remplacement sur PATCH, comme indiqué ci-dessous :

POST https://www.googleapis.com/...
X-HTTP-Method-Override: PATCH
...

Différence entre patch et update

En pratique, lorsque vous envoyez des données pour une requête de mise à jour utilisant le verbe HTTP PUT, vous ne devez envoyer que les champs obligatoires ou facultatifs. Si vous envoyez des valeurs de champs définis par le serveur, elles sont ignorées. Bien qu'elle puisse être perçue comme un autre moyen d'effectuer une mise à jour partielle, cette approche comporte certaines contraintes. Lorsque des mises à jour utilisent le verbe HTTP PUT, la requête échoue si vous ne fournissez pas les paramètres obligatoires. De plus, les données précédemment définies sont supprimées si vous ne fournissez pas les paramètres facultatifs.

C'est pourquoi il est plus prudent d'utiliser patch : vous ne fournissez que les données des champs à modifier, et les champs que vous omettez ne sont pas supprimés. La seule exception à cette règle concerne les éléments ou tableaux répétitifs. Si vous les omettez tous, ils ne sont pas modifiés, mais si vous en fournissez un, ils sont tous remplacés par l'ensemble fourni.

Taux de demandes maximal recommandé

Nous pouvons limiter la fréquence d'appel de l'API Google Wallet. Nous vous recommandons de ne pas dépasser 20 requêtes par seconde.

Envoyer des requêtes par lot à Google Wallet

L'API Google Wallet accepte les appels d'API par lot pour réduire le nombre de connexions qu'un client doit établir. Pour en savoir plus sur la structure des requêtes et des réponses par lot, consultez Informations concernant les lots.

L'exemple de code suivant représente des requêtes par lot. Les exemples Java et PHP utilisent les bibliothèques Google Wallet pour simplifier la création des classes et des objets.

Java

/**
 * Batch create Google Wallet objects from an existing class.
 *
 * @param issuerId The issuer ID being used for this request.
 * @param classSuffix Developer-defined unique ID for this pass class.
 */
public void batchCreateObjects(String issuerId, String classSuffix) throws IOException {
  // Create the batch request client
  BatchRequest batch = service.batch(new HttpCredentialsAdapter(credentials));

  // The callback will be invoked for each request in the batch
  JsonBatchCallback<TransitObject> callback =
      new JsonBatchCallback<TransitObject>() {
        // Invoked if the request was successful
        public void onSuccess(TransitObject response, HttpHeaders responseHeaders) {
          System.out.println("Batch insert response");
          System.out.println(response.toString());
        }

        // Invoked if the request failed
        public void onFailure(GoogleJsonError e, HttpHeaders responseHeaders) {
          System.out.println("Error Message: " + e.getMessage());
        }
      };

  // Example: Generate three new pass objects
  for (int i = 0; i < 3; i++) {
    // Generate a random object suffix
    String objectSuffix = UUID.randomUUID().toString().replaceAll("[^\\w.-]", "_");

    // See link below for more information on required properties
    // https://developers.google.com/wallet/tickets/transit-passes/qr-code/rest/v1/transitobject
    TransitObject batchObject =
        new TransitObject()
            .setId(String.format("%s.%s", issuerId, objectSuffix))
            .setClassId(String.format("%s.%s", issuerId, classSuffix))
            .setState("ACTIVE")
            .setHeroImage(
                new Image()
                    .setSourceUri(
                        new ImageUri()
                            .setUri(
                                "https://farm4.staticflickr.com/3723/11177041115_6e6a3b6f49_o.jpg"))
                    .setContentDescription(
                        new LocalizedString()
                            .setDefaultValue(
                                new TranslatedString()
                                    .setLanguage("en-US")
                                    .setValue("Hero image description"))))
            .setTextModulesData(
                    List.of(
                            new TextModuleData()
                                    .setHeader("Text module header")
                                    .setBody("Text module body")
                                    .setId("TEXT_MODULE_ID")))
            .setLinksModuleData(
                new LinksModuleData()
                    .setUris(
                        Arrays.asList(
                            new Uri()
                                .setUri("http://maps.google.com/")
                                .setDescription("Link module URI description")
                                .setId("LINK_MODULE_URI_ID"),
                            new Uri()
                                .setUri("tel:6505555555")
                                .setDescription("Link module tel description")
                                .setId("LINK_MODULE_TEL_ID"))))
            .setImageModulesData(
                    List.of(
                            new ImageModuleData()
                                    .setMainImage(
                                            new Image()
                                                    .setSourceUri(
                                                            new ImageUri()
                                                                    .setUri(
                                                                            "http://farm4.staticflickr.com/3738/12440799783_3dc3c20606_b.jpg"))
                                                    .setContentDescription(
                                                            new LocalizedString()
                                                                    .setDefaultValue(
                                                                            new TranslatedString()
                                                                                    .setLanguage("en-US")
                                                                                    .setValue("Image module description"))))
                                    .setId("IMAGE_MODULE_ID")))
            .setBarcode(new Barcode().setType("QR_CODE").setValue("QR code value"))
            .setLocations(
                    List.of(
                            new LatLongPoint()
                                    .setLatitude(37.424015499999996)
                                    .setLongitude(-122.09259560000001)))
            .setPassengerType("SINGLE_PASSENGER")
            .setPassengerNames("Passenger names")
            .setTripType("ONE_WAY")
            .setTicketLeg(
                new TicketLeg()
                    .setOriginStationCode("LA")
                    .setOriginName(
                        new LocalizedString()
                            .setDefaultValue(
                                new TranslatedString()
                                    .setLanguage("en-US")
                                    .setValue("Origin name")))
                    .setDestinationStationCode("SFO")
                    .setDestinationName(
                        new LocalizedString()
                            .setDefaultValue(
                                new TranslatedString()
                                    .setLanguage("en-US")
                                    .setValue("Origin name")))
                    .setDepartureDateTime("2020-04-12T16:20:50.52Z")
                    .setArrivalDateTime("2020-04-12T20:20:50.52Z")
                    .setFareName(
                        new LocalizedString()
                            .setDefaultValue(
                                new TranslatedString()
                                    .setLanguage("en-US")
                                    .setValue("Fare name"))));

    service.transitobject().insert(batchObject).queue(batch, callback);
  }

  // Invoke the batch API calls
  batch.execute();
}

PHP

/**
 * Batch create Google Wallet objects from an existing class.
 *
 * @param string $issuerId The issuer ID being used for this request.
 * @param string $classSuffix Developer-defined unique ID for the pass class.
 */
public function batchCreateObjects(string $issuerId, string $classSuffix)
{
  // Update the client to enable batch requests
  $this->client->setUseBatch(true);
  $batch = $this->service->createBatch();

  // Example: Generate three new pass objects
  for ($i = 0; $i < 3; $i++) {
    // Generate a random object suffix
    $objectSuffix = preg_replace('/[^\w.-]/i', '_', uniqid());

    // See link below for more information on required properties
    // https://developers.google.com/wallet/tickets/transit-passes/qr-code/rest/v1/transitobject
    $batchObject = new Google_Service_Walletobjects_TransitObject([
      'id' => "{$issuerId}.{$objectSuffix}",
      'classId' => "{$issuerId}.{$classSuffix}",
      'state' => 'ACTIVE',
      'heroImage' => new Google_Service_Walletobjects_Image([
        'sourceUri' => new Google_Service_Walletobjects_ImageUri([
          'uri' => 'https://farm4.staticflickr.com/3723/11177041115_6e6a3b6f49_o.jpg'
        ]),
        'contentDescription' => new Google_Service_Walletobjects_LocalizedString([
          'defaultValue' => new Google_Service_Walletobjects_TranslatedString([
            'language' => 'en-US',
            'value' => 'Hero image description'
          ])
        ])
      ]),
      'textModulesData' => [
        new Google_Service_Walletobjects_TextModuleData([
          'header' => 'Text module header',
          'body' => 'Text module body',
          'id' => 'TEXT_MODULE_ID'
        ])
      ],
      'linksModuleData' => new Google_Service_Walletobjects_LinksModuleData([
        'uris' => [
          new Google_Service_Walletobjects_Uri([
            'uri' => 'http://maps.google.com/',
            'description' => 'Link module URI description',
            'id' => 'LINK_MODULE_URI_ID'
          ]),
          new Google_Service_Walletobjects_Uri([
            'uri' => 'tel:6505555555',
            'description' => 'Link module tel description',
            'id' => 'LINK_MODULE_TEL_ID'
          ])
        ]
      ]),
      'imageModulesData' => [
        new Google_Service_Walletobjects_ImageModuleData([
          'mainImage' => new Google_Service_Walletobjects_Image([
            'sourceUri' => new Google_Service_Walletobjects_ImageUri([
              'uri' => 'http://farm4.staticflickr.com/3738/12440799783_3dc3c20606_b.jpg'
            ]),
            'contentDescription' => new Google_Service_Walletobjects_LocalizedString([
              'defaultValue' => new Google_Service_Walletobjects_TranslatedString([
                'language' => 'en-US',
                'value' => 'Image module description'
              ])
            ])
          ]),
          'id' => 'IMAGE_MODULE_ID'
        ])
      ],
      'barcode' => new Google_Service_Walletobjects_Barcode([
        'type' => 'QR_CODE',
        'value' => 'QR code value'
      ]),
      'locations' => [
        new Google_Service_Walletobjects_LatLongPoint([
          'latitude' => 37.424015499999996,
          'longitude' =>  -122.09259560000001
        ])
      ],
      'passengerType' => 'SINGLE_PASSENGER',
      'passengerNames' => 'Passenger names',
      'tripType' => 'ONE_WAY',
      'ticketLeg' => new Google_Service_Walletobjects_TicketLeg([
        'originStationCode' => 'LA',
        'originName' => new Google_Service_Walletobjects_LocalizedString([
          'defaultValue' => new Google_Service_Walletobjects_TranslatedString([
            'language' => 'en-US',
            'value' => 'Origin name'
          ])
        ]),
        'destinationStationCode' => 'SFO',
        'destinationName' => new Google_Service_Walletobjects_LocalizedString([
          'defaultValue' => new Google_Service_Walletobjects_TranslatedString([
            'language' => 'en-US',
            'value' => 'Destination name'
          ])
        ]),
        'departureDateTime' => '2020-04-12T16:20:50.52Z',
        'arrivalDateTime' => '2020-04-12T20:20:50.52Z',
        'fareName' => new Google_Service_Walletobjects_LocalizedString([
          'defaultValue' => new Google_Service_Walletobjects_TranslatedString([
            'language' => 'en-US',
            'value' => 'Fare name'
          ])
        ])
      ])
    ]);

    $batch->add($this->service->transitobject->insert($batchObject));
  }

  // Make the batch request
  $batchResponse = $batch->execute();

  print "Batch insert response\n";
  foreach ($batchResponse as $key => $value) {
    if ($value instanceof Google_Service_Exception) {
      print_r($value->getErrors());
      continue;
    }
    print "{$value->getId()}\n";
  }
}

Python

def batch_create_objects(self, issuer_id: str, class_suffix: str):
    """Batch create Google Wallet objects from an existing class.

    The request body will be a multiline string. See below for information.

    https://cloud.google.com/compute/docs/api/how-tos/batch#example

    Args:
        issuer_id (str): The issuer ID being used for this request.
        class_suffix (str): Developer-defined unique ID for this pass class.
    """
    data = ''

    # Example: Generate three new pass objects
    for _ in range(3):
        # Generate a random object suffix
        object_suffix = str(uuid.uuid4()).replace('[^\\w.-]', '_')

        # See link below for more information on required properties
        # https://developers.google.com/wallet/tickets/transit-passes/qr-code/rest/v1/transitobject
        batch_object = {
            'id': f'{issuer_id}.{object_suffix}',
            'classId': f'{issuer_id}.{class_suffix}',
            'state': 'ACTIVE',
            'heroImage': {
                'sourceUri': {
                    'uri':
                        'https://farm4.staticflickr.com/3723/11177041115_6e6a3b6f49_o.jpg'
                },
                'contentDescription': {
                    'defaultValue': {
                        'language': 'en-US',
                        'value': 'Hero image description'
                    }
                }
            },
            'textModulesData': [{
                'header': 'Text module header',
                'body': 'Text module body',
                'id': 'TEXT_MODULE_ID'
            }],
            'linksModuleData': {
                'uris': [{
                    'uri': 'http://maps.google.com/',
                    'description': 'Link module URI description',
                    'id': 'LINK_MODULE_URI_ID'
                }, {
                    'uri': 'tel:6505555555',
                    'description': 'Link module tel description',
                    'id': 'LINK_MODULE_TEL_ID'
                }]
            },
            'imageModulesData': [{
                'mainImage': {
                    'sourceUri': {
                        'uri':
                            'http://farm4.staticflickr.com/3738/12440799783_3dc3c20606_b.jpg'
                    },
                    'contentDescription': {
                        'defaultValue': {
                            'language': 'en-US',
                            'value': 'Image module description'
                        }
                    }
                },
                'id': 'IMAGE_MODULE_ID'
            }],
            'barcode': {
                'type': 'QR_CODE',
                'value': 'QR code'
            },
            'locations': [{
                'latitude': 37.424015499999996,
                'longitude': -122.09259560000001
            }],
            'passengerType': 'SINGLE_PASSENGER',
            'passengerNames': 'Passenger names',
            'tripType': 'ONE_WAY',
            'ticketLeg': {
                'originStationCode': 'LA',
                'originName': {
                    'defaultValue': {
                        'language': 'en-US',
                        'value': 'Origin name'
                    }
                },
                'destinationStationCode': 'SFO',
                'destinationName': {
                    'defaultValue': {
                        'language': 'en-US',
                        'value': 'Destination name'
                    }
                },
                'departureDateTime': '2020-04-12T16:20:50.52Z',
                'arrivalDateTime': '2020-04-12T20:20:50.52Z',
                'fareName': {
                    'defaultValue': {
                        'language': 'en-US',
                        'value': 'Fare name'
                    }
                }
            }
        }

        data += '--batch_createobjectbatch\n'
        data += 'Content-Type: application/json\n\n'
        data += 'POST /walletobjects/v1/transitObject/\n\n'

        data += json.dumps(batch_object) + '\n\n'

    data += '--batch_createobjectbatch--'

    # Invoke the batch API calls
    response = self.http_client.post(
        url='https://walletobjects.googleapis.com/batch',
        data=data,
        headers={
            # `boundary` is the delimiter between API calls in the batch request
            'Content-Type':
                'multipart/mixed; boundary=batch_createobjectbatch'
        })

    print('Batch insert response')
    print(response.content.decode('UTF-8'))

C#

/// <summary>
/// Batch create Google Wallet objects from an existing class.
/// </summary>
/// <param name="issuerId">The issuer ID being used for this request.</param>
/// <param name="classSuffix">Developer-defined unique ID for this pass class.</param>
public async void BatchCreateObjects(string issuerId, string classSuffix)
{
  // The request body will be a multiline string
  // See below for more information
  // https://cloud.google.com/compute/docs/api/how-tos/batch//example
  string data = "";

  HttpClient httpClient = new HttpClient();
  httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Bearer",
    credentials.GetAccessTokenForRequestAsync().Result
  );

  // Example: Generate three new pass objects
  for (int i = 0; i < 3; i++)
  {
    // Generate a random object suffix
    string objectSuffix = Regex.Replace(Guid.NewGuid().ToString(), "[^\\w.-]", "_");

    // See link below for more information on required properties
    // https://developers.google.com/wallet/tickets/transit-passes/qr-code/rest/v1/transitobject
    TransitObject batchObject = new TransitObject
    {
      Id = $"{issuerId}.{objectSuffix}",
      ClassId = $"{issuerId}.{classSuffix}",
      State = "ACTIVE",
      HeroImage = new Image
      {
        SourceUri = new ImageUri
        {
          Uri = "https://farm4.staticflickr.com/3723/11177041115_6e6a3b6f49_o.jpg"
        },
        ContentDescription = new LocalizedString
        {
          DefaultValue = new TranslatedString
          {
            Language = "en-US",
            Value = "Hero image description"
          }
        }
      },
      TextModulesData = new List<TextModuleData>
      {
        new TextModuleData
        {
          Header = "Text module header",
          Body = "Text module body",
          Id = "TEXT_MODULE_ID"
        }
      },
      LinksModuleData = new LinksModuleData
      {
        Uris = new List<Google.Apis.Walletobjects.v1.Data.Uri>
        {
          new Google.Apis.Walletobjects.v1.Data.Uri
          {
            UriValue = "http://maps.google.com/",
            Description = "Link module URI description",
            Id = "LINK_MODULE_URI_ID"
          },
          new Google.Apis.Walletobjects.v1.Data.Uri
          {
            UriValue = "tel:6505555555",
            Description = "Link module tel description",
            Id = "LINK_MODULE_TEL_ID"
          }
        }
      },
      ImageModulesData = new List<ImageModuleData>
      {
        new ImageModuleData
        {
          MainImage = new Image
          {
            SourceUri = new ImageUri
            {
              Uri = "http://farm4.staticflickr.com/3738/12440799783_3dc3c20606_b.jpg"
            },
            ContentDescription = new LocalizedString
            {
              DefaultValue = new TranslatedString
              {
                Language = "en-US",
                Value = "Image module description"
              }
            }
          },
          Id = "IMAGE_MODULE_ID"
        }
      },
      Barcode = new Barcode
      {
        Type = "QR_CODE",
        Value = "QR code"
      },
      Locations = new List<LatLongPoint>
      {
        new LatLongPoint
        {
          Latitude = 37.424015499999996,
          Longitude = -122.09259560000001
        }
      },
      PassengerType = "SINGLE_PASSENGER",
      PassengerNames = "Passenger names",
      TripType = "ONE_WAY",
      TicketLeg = new TicketLeg
      {
        OriginStationCode = "LA",
        OriginName = new LocalizedString
        {
          DefaultValue = new TranslatedString
          {
            Language = "en-US",
            Value = "Origin name"
          }
        },
        DestinationStationCode = "SFO",
        DestinationName = new LocalizedString
        {
          DefaultValue = new TranslatedString
          {
            Language = "en-US",
            Value = "Destination name"
          }
        },
        DepartureDateTime = "2020-04-12T16:20:50.52Z",
        ArrivalDateTime = "2020-04-12T20:20:50.52Z",
        FareName = new LocalizedString
        {
          DefaultValue = new TranslatedString
          {
            Language = "en-US",
            Value = "Fare name"
          }
        }
      }
    };

    data += "--batch_createobjectbatch\n";
    data += "Content-Type: application/json\n\n";
    data += "POST /walletobjects/v1/transitObject/\n\n";

    data += JsonConvert.SerializeObject(batchObject) + "\n\n";
  }
  data += "--batch_createobjectbatch--";

  // Invoke the batch API calls
  HttpRequestMessage batchObjectRequest = new HttpRequestMessage(
      HttpMethod.Post,
      "https://walletobjects.googleapis.com/batch");

  batchObjectRequest.Content = new StringContent(data);
  batchObjectRequest.Content.Headers.ContentType = new MediaTypeHeaderValue(
      "multipart/mixed");
  // `boundary` is the delimiter between API calls in the batch request
  batchObjectRequest.Content.Headers.ContentType.Parameters.Add(
      new NameValueHeaderValue("boundary", "batch_createobjectbatch"));

  HttpResponseMessage batchObjectResponse = httpClient.Send(
      batchObjectRequest);

  string batchObjectContent = await batchObjectResponse
      .Content
      .ReadAsStringAsync();

  Console.WriteLine("Batch insert response");
  Console.WriteLine(batchObjectContent);
}

Node.js

/**
 * Batch create Google Wallet objects from an existing class.
 *
 * @param {string} issuerId The issuer ID being used for this request.
 * @param {string} classSuffix Developer-defined unique ID for this pass class.
 */
async batchCreateObjects(issuerId, classSuffix) {
  // See below for more information
  // https://cloud.google.com/compute/docs/api/how-tos/batch#example
  let data = '';
  let batchObject;
  let objectSuffix;

  // Example: Generate three new pass objects
  for (let i = 0; i < 3; i++) {
    // Generate a random object suffix
    objectSuffix = uuidv4().replace('[^\w.-]', '_');

    // See link below for more information on required properties
    // https://developers.google.com/wallet/tickets/transit-passes/qr-code/rest/v1/transitobject
    batchObject = {
      'id': `${issuerId}.${objectSuffix}`,
      'classId': `${issuerId}.${classSuffix}`,
      'state': 'ACTIVE',
      'heroImage': {
        'sourceUri': {
          'uri': 'https://farm4.staticflickr.com/3723/11177041115_6e6a3b6f49_o.jpg'
        },
        'contentDescription': {
          'defaultValue': {
            'language': 'en-US',
            'value': 'Hero image description'
          }
        }
      },
      'textModulesData': [
        {
          'header': 'Text module header',
          'body': 'Text module body',
          'id': 'TEXT_MODULE_ID'
        }
      ],
      'linksModuleData': {
        'uris': [
          {
            'uri': 'http://maps.google.com/',
            'description': 'Link module URI description',
            'id': 'LINK_MODULE_URI_ID'
          },
          {
            'uri': 'tel:6505555555',
            'description': 'Link module tel description',
            'id': 'LINK_MODULE_TEL_ID'
          }
        ]
      },
      'imageModulesData': [
        {
          'mainImage': {
            'sourceUri': {
              'uri': 'http://farm4.staticflickr.com/3738/12440799783_3dc3c20606_b.jpg'
            },
            'contentDescription': {
              'defaultValue': {
                'language': 'en-US',
                'value': 'Image module description'
              }
            }
          },
          'id': 'IMAGE_MODULE_ID'
        }
      ],
      'barcode': {
        'type': 'QR_CODE',
        'value': 'QR code'
      },
      'locations': [
        {
          'latitude': 37.424015499999996,
          'longitude': -122.09259560000001
        }
      ],
      'passengerType': 'SINGLE_PASSENGER',
      'passengerNames': 'Passenger names',
      'tripType': 'ONE_WAY',
      'ticketLeg': {
        'originStationCode': 'LA',
        'originName': {
          'defaultValue': {
            'language': 'en-US',
            'value': 'Origin name'
          }
        },
        'destinationStationCode': 'SFO',
        'destinationName': {
          'defaultValue': {
            'language': 'en-US',
            'value': 'Destination name'
          }
        },
        'departureDateTime': '2020-04-12T16:20:50.52Z',
        'arrivalDateTime': '2020-04-12T20:20:50.52Z',
        'fareName': {
          'defaultValue': {
            'language': 'en-US',
            'value': 'Fare name'
          }
        }
      }
    };

    data += '--batch_createobjectbatch\n';
    data += 'Content-Type: application/json\n\n';
    data += 'POST /walletobjects/v1/transitObject\n\n';

    data += JSON.stringify(batchObject) + '\n\n';
  }
  data += '--batch_createobjectbatch--';

  // Invoke the batch API calls
  let response = await this.httpClient.request({
    url: this.batchUrl,
    method: 'POST',
    data: data,
    headers: {
      // `boundary` is the delimiter between API calls in the batch request
      'Content-Type': 'multipart/mixed; boundary=batch_createobjectbatch'
    }
  });

  console.log('Batch insert response');
  console.log(response);
}