In the following tutorial we will demonstrate how to build a feed in JSON using the Google Protocol Buffers (protobuf) definition. We will use the protobuf compiler to generate source code based on the protobuf schema. Building a feed using the generated source code provides an easy interface and prevents feed entities from being created with incorrect field names or field types.
Project setup
- Create a new project directory.
- Copy the contents of offer.proto money.proto dayofweek.proto timeofday.proto date.proto from the proto definitions as new files to your project directory root.
- Install the protoc compiler
In order to generate source code from the .proto files, you will need the protoc compiler. Download the latest pre-built binary from the Protocol Buffers GitHub release page.
Extract the zip and add the bin path to the PATH environment variable, for example:
unzip protoc-22.0-linux-x86_64.zip -d /usr/local/protoc export PATH="$PATH:/usr/local/protoc/bin"
Generate source code
Python
- Install the Python protobuf library
pip install protobuf
- Generate client source code.
Create the proto output directory: generated
protoc --python_out=./generated offer.proto money.proto dayofweek.proto timeofday.proto date.proto
Update the PYTHONPATH environment variable to include the generated path for example:
export PYTHONPATH="$PYTHONPATH:./generated"
If you are using Bazel, try the py_proto_library rule as an alternative to running protoc.
Usage example
Full example project can be found here.
import json from google.protobuf.json_format import MessageToDict from google.protobuf.timestamp_pb2 import Timestamp # Replace these imports with your actual generated proto package paths from generated import offer_pb2 from generated import money_pb2 from generated import dayofweek_pb2 from generated import timeofday_pb2 _MAX_BYTES_DATA_FILE = 200 * 1024 * 1024 def generate_offer_feed(): # Create OfferFeed feed = offer_pb2.OfferFeed() # Build the offers offer = offer_pb2.Offer( offer_id="offer-1", entity_ids=["dining-1"], offer_source=offer_pb2.OFFER_SOURCE_AGGREGATOR, action_type=offer_pb2.ACTION_TYPE_DINING, offer_modes=[ offer_pb2.OFFER_MODE_WALK_IN, offer_pb2.OFFER_MODE_FREE_RESERVATION ], offer_category=offer_pb2.OFFER_CATEGORY_BASE_OFFER, offer_details=offer_pb2.OfferDetails( offer_display_text="₹100 off on your order", # Note: If this is a 'oneof', you set the field name directly discount_value=money_pb2.Money( currency_code="INR", units=100 ) ), offer_restrictions=offer_pb2.OfferRestrictions( combinable_with_other_offers=True, combinable_offer_categories=[ offer_pb2.OFFER_CATEGORY_ADD_ON_PAYMENT_OFFER, offer_pb2.OFFER_CATEGORY_ADD_ON_COUPON_OFFER ] ), terms=offer_pb2.Terms( restricted_to_certain_users=False, terms_and_conditions="Valid on all menu items." ), validity_periods=[ offer_pb2.ValidityPeriod( valid_period=offer_pb2.ValidityRange( valid_from_time=Timestamp(seconds=1687062000), valid_through_time=Timestamp(seconds=1956556800) ), time_of_day=[ # Monday - Thursday Window offer_pb2.TimeOfDayWindow( time_windows=offer_pb2.TimeOfDayRange( open_time=timeofday_pb2.TimeOfDay(hours=13), close_time=timeofday_pb2.TimeOfDay(hours=23) ), day_of_week=[ dayofweek_pb2.DayOfWeek.MONDAY, dayofweek_pb2.DayOfWeek.TUESDAY, dayofweek_pb2.DayOfWeek.WEDNESDAY, dayofweek_pb2.DayOfWeek.THURSDAY ] ), # Friday - Sunday Window offer_pb2.TimeOfDayWindow( time_windows=offer_pb2.TimeOfDayRange( open_time=timeofday_pb2.TimeOfDay(hours=13), close_time=timeofday_pb2.TimeOfDay(hours=23, minutes=59, seconds=59) ), day_of_week=[ dayofweek_pb2.DayOfWeek.FRIDAY, dayofweek_pb2.DayOfWeek.SATURDAY, dayofweek_pb2.DayOfWeek.SUNDAY ] ) ] ) ], offer_url="https://www.example-restaurant.com/offer/base_offer_1", image_url="https://www.example-restaurant.com/images/offer_base.jpg" ) # Example testing for menu feed size # Protocol buffer message must be less than 2 GiB # https://protobuf.dev/programming-guides/proto-limits/ # It is recommended to not exceed 200 MB, as there is an Actions # Center limit of 200 MB per file after compression. if feed.ByteSize() + offer.ByteSize() < _MAX_BYTES_DATA_FILE: feed.data.append(offer) # else write current feed to file and start a new feed # Serialize to JSON # preserving_proto_field_names=True ensures camelCase becomes snake_case if defined that way in .proto json_output = json.dumps( MessageToDict(feed, preserving_proto_field_name=True) ) print(json_output) if __name__ == "__main__": generate_offer_feed()
The code example demonstrates how to create a DINING offer feed. The example then shows how to serialize the feed to JSON.
The Python api allows lazy initialization of nested objects by setting the properties.Java
- Add the dependencies protobuf-java and protobuf-java-util to your project using either maven or gradle as described here.
- Generate client source code.
protoc --java_out=src/main/java offer.proto money.proto dayofweek.proto timeofday.proto date.proto
You may use the protobuf-maven-plugin to generate the source code during compile time when using maven.
If you are using Bazel, try the java_proto_library rule as an alternative to running protoc.
Usage example
Full example project can be found here.
package com.example.offers; import com.google.protobuf.util.JsonFormat; import ext.maps.booking.feeds.offers.Offer; import ext.maps.booking.feeds.offers.OfferFeed; import ext.maps.booking.feeds.offers.OfferDetails; import ext.maps.booking.feeds.offers.OfferRestrictions; import ext.maps.booking.feeds.offers.Terms; import ext.maps.booking.feeds.offers.ValidityPeriod; import ext.maps.booking.feeds.offers.ValidityRange; import ext.maps.booking.feeds.offers.OfferCategory; import ext.maps.booking.feeds.offers.OfferMode; import ext.maps.booking.feeds.offers.OfferSource; import ext.maps.booking.feeds.offers.ActionType; import ext.maps.booking.feeds.offers.TimeOfDayWindow; import ext.maps.booking.feeds.offers.TimeOfDayRange; import com.google.type.Money; import com.google.protobuf.Timestamp; import com.google.type.DayOfWeek; import com.google.type.TimeOfDay; public class OfferFeedGenerator { // 200 MB public static final int MAX_BYTES_DATA_FILE = 200 * 1024 * 1024; public static void main(String[] args) throws Exception { // Create OfferFeed OfferFeed.Builder feed = OfferFeed.newBuilder(); // Build the offers Offer offer = Offer.newBuilder() .setOfferId("offer-1") .addEntityIds("dining-1") .setOfferSource(OfferSource.OFFER_SOURCE_AGGREGATOR) .setActionType(ActionType.ACTION_TYPE_DINING) .addOfferModes(OfferMode.OFFER_MODE_WALK_IN) .addOfferModes(OfferMode.OFFER_MODE_FREE_RESERVATION) .setOfferCategory(OfferCategory.OFFER_CATEGORY_BASE_OFFER) .setOfferDetails(OfferDetails.newBuilder() .setOfferDisplayText("₹100 off on your order") .setDiscountValue(Money.newBuilder() .setCurrencyCode("INR") .setUnits(100))) .setOfferRestrictions(OfferRestrictions.newBuilder() .setCombinableWithOtherOffers(true) .addCombinableOfferCategories(OfferCategory.OFFER_CATEGORY_ADD_ON_PAYMENT_OFFER) .addCombinableOfferCategories(OfferCategory.OFFER_CATEGORY_ADD_ON_COUPON_OFFER)) .setTerms(Terms.newBuilder() .setRestrictedToCertainUsers(false) .setTermsAndConditions("Valid on all menu items.")) .addValidityPeriods(ValidityPeriod.newBuilder() .setValidPeriod(ext.maps.booking.feeds.offers.ValidityRange.newBuilder() .setValidFromTime(Timestamp.newBuilder().setSeconds(1687062000)) .setValidThroughTime(Timestamp.newBuilder().setSeconds(1956556800))) // // Monday - Thursday Window .addTimeOfDay(ext.maps.booking.feeds.offers.TimeOfDayWindow.newBuilder() .setTimeWindows(ext.maps.booking.feeds.offers.TimeOfDayRange.newBuilder() .setOpenTime(com.google.type.TimeOfDay.newBuilder().setHours(13)) .setCloseTime(com.google.type.TimeOfDay.newBuilder().setHours(23))) .addDayOfWeek(DayOfWeek.MONDAY) .addDayOfWeek(DayOfWeek.TUESDAY) .addDayOfWeek(DayOfWeek.WEDNESDAY) .addDayOfWeek(DayOfWeek.THURSDAY)) // Friday - Sunday Window .addTimeOfDay(ext.maps.booking.feeds.offers.TimeOfDayWindow.newBuilder() .setTimeWindows(ext.maps.booking.feeds.offers.TimeOfDayRange.newBuilder() .setOpenTime(com.google.type.TimeOfDay.newBuilder().setHours(13)) .setCloseTime(com.google.type.TimeOfDay.newBuilder().setHours(23).setMinutes(59).setSeconds(59))) .addDayOfWeek(DayOfWeek.FRIDAY) .addDayOfWeek(DayOfWeek.SATURDAY) .addDayOfWeek(DayOfWeek.SUNDAY)) ) .setOfferUrl("https://www.example-restaurant.com/offer/base_offer_1") .setImageUrl("https://www.example-restaurant.com/images/offer_base.jpg") .build(); // Example testing for offer feed size // Protocol buffer message must be less than 2 GiB // https://protobuf.dev/programming-guides/proto-limits/ // It is recommended to not exceed 200 MB, as there is an Actions // Center limit of 200 MB per file after compression. int offerSize = offer.getSerializedSize(); int currentFeedSize = feed.build().getSerializedSize(); if (currentFeedSize + offerSize < MAX_BYTES_DATA_FILE) { feed.addData(offer); } else { // 1. Serialize and save the current 'feed' to a file // 2. Reset 'feed' to a new empty OfferFeed // 3. Add the 'offer' to the new feed } // Serialize to JSON String jsonOutput = JsonFormat.printer() .preservingProtoFieldNames() .print(feed); System.out.println(jsonOutput); } }
The code example demonstrates how to create a DINING offer feed. The example then shows how to serialize the feed to JSON.
Go
- Install the protoc plugin for go
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
Update the PATH environment variable to include the protoc-gen-go plugin, for example:
export PATH="$PATH:$(go env GOPATH)/bin"
- Initialize app and generate client source code.
go mod init feed/app mkdir generated protoc --go_out=./generated/ offer.proto money.proto dayofweek.proto timeofday.proto date.proto
If you are using Bazel, try the go_proto_library rule as an alternative to running protoc.
Usage example
Full example project can be found here.
package main import ( pb "feed/app/generated/ext/maps/booking/offers/proto" "fmt" "log" money "google.golang.org/genproto/googleapis/type/money" timestamppb "google.golang.org/protobuf/types/known/timestamppb" dayofweek "google.golang.org/genproto/googleapis/type/dayofweek" timeofday "google.golang.org/genproto/googleapis/type/timeofday" proto "google.golang.org/protobuf/proto" "google.golang.org/protobuf/encoding/protojson" ) func main() { // 200 MB feed file size limit const MaxBytesDataFile = 200 * 1024 * 1024 // Create OfferFeed with offers feed := &pb.OfferFeed{} // Build the offers offer := &pb.Offer{ OfferId: "offer-1", EntityIds: []string{"dining-1"}, OfferSource: pb.OfferSource_OFFER_SOURCE_AGGREGATOR, ActionType: pb.ActionType_ACTION_TYPE_DINING, OfferModes: []pb.OfferMode{ pb.OfferMode_OFFER_MODE_WALK_IN, pb.OfferMode_OFFER_MODE_FREE_RESERVATION, }, OfferCategory: pb.OfferCategory_OFFER_CATEGORY_BASE_OFFER, OfferDetails: &pb.OfferDetails{ OfferDisplayText: "₹100 off on your order", OfferSpecification: &pb.OfferDetails_DiscountValue{ DiscountValue: &money.Money{ CurrencyCode: "INR", Units: 100, }, }, }, OfferRestrictions: &pb.OfferRestrictions{ CombinableWithOtherOffers: true, CombinableOfferCategories: []pb.OfferCategory{ pb.OfferCategory_OFFER_CATEGORY_ADD_ON_PAYMENT_OFFER, pb.OfferCategory_OFFER_CATEGORY_ADD_ON_COUPON_OFFER, }, }, Terms: &pb.Terms{ RestrictedToCertainUsers: false, TermsAndConditions: "Valid on all menu items.", }, ValidityPeriods: []*pb.ValidityPeriod{ { ValidPeriod: &pb.ValidityRange{ ValidFromTime: ×tamppb.Timestamp{Seconds: 1687062000}, ValidThroughTime: ×tamppb.Timestamp{Seconds: 1956556800}, }, TimeOfDay: []*pb.TimeOfDayWindow{ // Monday - Thursday Window { TimeWindows: &pb.TimeOfDayRange{ OpenTime: &timeofday.TimeOfDay{Hours: 13}, CloseTime: &timeofday.TimeOfDay{Hours: 23}, }, DayOfWeek: []dayofweek.DayOfWeek{ dayofweek.DayOfWeek_MONDAY, dayofweek.DayOfWeek_TUESDAY, dayofweek.DayOfWeek_WEDNESDAY, dayofweek.DayOfWeek_THURSDAY, }, }, // Friday - Sunday Window { TimeWindows: &pb.TimeOfDayRange{ OpenTime: &timeofday.TimeOfDay{Hours: 13}, CloseTime: &timeofday.TimeOfDay{ Hours: 23, Minutes: 59, Seconds: 59, }, }, DayOfWeek: []dayofweek.DayOfWeek{ dayofweek.DayOfWeek_FRIDAY, dayofweek.DayOfWeek_SATURDAY, dayofweek.DayOfWeek_SUNDAY, }, }, }, }, }, OfferUrl: "https://www.example-restaurant.com/offer/base_offer_1", ImageUrl: "https://www.example-restaurant.com/images/offer_base.jpg", } // Example testing for feed size // Protocol buffer message must be less than 2 GiB // https://protobuf.dev/programming-guides/proto-limits/ // It is recommended to not exceed 200 MB, as there is an Actions // Center limit of 200 MB per file after compression. offerSize := proto.Size(offer) currentFeedSize := proto.Size(feed) if currentFeedSize + offerSize < MaxBytesDataFile { feed.Data = append(feed.Data, offer) } else { // 1. Serialize and save the current 'feed' to a file // 2. Reset 'feed' to a new empty OfferFeed // 3. Add the 'offer' to the new feed } // Serialize to JSON marshaler := protojson.MarshalOptions{ UseProtoNames: true, } jsonOutput, err := marshaler.Marshal(feed) if err != nil { log.Fatalf("Failed to marshal feed: %v", err) } fmt.Println(string(jsonOutput)) }
The code example demonstrates how to create a DINING offer feed. The example then shows how to serialize the feed to JSON.
TypeScript
-
Install the TypeScript protoc plugin.
Note that ts-proto is not an officially supported Google project.npm init npm i -D typescript npm i ts-proto
- Create the output directory and generate client source code.
Create the proto output directory: src/generated
protoc --plugin="./node_modules/.bin/protoc-gen-ts_proto" --ts_proto_opt=useOptionals=all --ts_proto_opt=snakeToCamel=false --ts_proto_opt=onlyTypes=true --ts_proto_out="./src/generated" offer.proto money.proto dayofweek.proto timeofday.proto date.proto
If you are using Bazel, try the js_proto_library rule as an alternative to running protoc.
Usage example
Full example project can be found here.
import { Offer, OfferFeed, OfferSource, ActionType, OfferMode, OfferCategory } from "./generated/offer"; // Path to your generated types import { Money } from "./generated/money"; import { DayOfWeek } from "./generated/dayofweek"; import { TimeOfDay } from "./generated/timeofday"; import { Timestamp } from "./generated/google/protobuf/timestamp"; import { Duration } from "./generated/google/protobuf/duration"; // 200 MB Limit const MAX_BYTES_DATA_FILE = 200 * 1024 * 1024; function generateOfferFeed() { // Build the Offer object const offer: Offer = { offer_id: "offer-1", entity_ids: ["dining-1"], offer_source: OfferSource.OFFER_SOURCE_AGGREGATOR, action_type: ActionType.ACTION_TYPE_DINING, offer_modes: [ OfferMode.OFFER_MODE_WALK_IN, OfferMode.OFFER_MODE_FREE_RESERVATION, ], offer_category: OfferCategory.OFFER_CATEGORY_BASE_OFFER, offer_details: { offer_display_text: "₹100 off on your order", // Set 'oneof' field: discountValue discount_value: { currency_code: "INR", units: 100, }, }, offer_restrictions: { combinable_with_other_offers: true, combinable_offer_categories: [ OfferCategory.OFFER_CATEGORY_ADD_ON_PAYMENT_OFFER, OfferCategory.OFFER_CATEGORY_ADD_ON_COUPON_OFFER, ], }, terms: { restricted_to_certain_users: false, terms_and_conditions: "Valid on all menu items.", }, validity_periods: [ { valid_period: { valid_from_time: new Date(1687062000000), valid_through_time: new Date(1956556800000), }, time_of_day: [ // Monday - Thursday Window { time_windows: { open_time: { hours: 13, minutes: 0, seconds: 0, nanos: 0 }, close_time: { hours: 23, minutes: 0, seconds: 0, nanos: 0 }, }, day_of_week: [ DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY, DayOfWeek.THURSDAY, ], }, // Friday - Sunday Window { time_windows: { open_time: { hours: 13, minutes: 0, seconds: 0, nanos: 0 }, close_time: { hours: 23, minutes: 59, seconds: 59, nanos: 0 }, }, day_of_week: [ DayOfWeek.FRIDAY, DayOfWeek.SATURDAY, DayOfWeek.SUNDAY, ], }, ], }, ], offer_url: "https://www.example-restaurant.com/offer/base_offer_1", image_url: "https://www.example-restaurant.com/images/offer_base.jpg", }; // 2. Initialize the Feed const feed: OfferFeed = { data: [] }; // 3. Size checking // encode().finish() returns a Uint8Array (the binary representation) const offerSize = new Blob([JSON.stringify(offer)]).size; const currentFeedSize = new Blob([JSON.stringify(feed)]).size; if (currentFeedSize + offerSize < MAX_BYTES_DATA_FILE) { if (feed.data == undefined) { feed.data = [offer]; } else { feed.data.push(offer); } } else { // Logic for file rotation goes here console.log("Feed size limit reached. Rotate file."); } // 4. Serialize to JSON // In JS/TS, we often just use JSON.stringify for the plain object // if you used proto3 and want to ensure proper enum/timestamp formatting, // use the library's toJSON method. const jsonOutput = JSON.stringify(feed); console.log(jsonOutput); } generateOfferFeed();
The code example demonstrates how to create a DINING offer feed. The example then shows how to serialize the feed to JSON.
Note: There are third party protoc compiler extensions to generate JSON Schema from the proto schema. If you prefer to work with JSON Schema files, you can review one such example here.