In this example we are going to create a gRPC client and server application. The client will send a complex multidimensional JSON request message and receive a simple response message from the server. Some fields will have their proto messages and some will be completely unknown/random fields.


There are two important packages we are using here. protobuf protobuf package for handling complex fields. jsonpb package for handling JSON marshalling and unmarshalling. Scroll at the bottom for the links.


Request


This is what we are exactly sending. It will be read from a file just to keep the blog short. It is stored as client/request.json within the application.


{
"name": "The Premier League",
"founded_at": "1992-02-200T22:40:36Z",
"is_active": true,
"budget": 1234567890.99,
"mascot": "Lion",
"stadiums": {
"City Ground": "Nottingham Forest",
"Manchester United": "Old Trafford",
"National": "Wembley Stadium"
},
"sponsors": [
"EA Sports",
"Coca Cola",
"Nike"
],
"awards": [
{
"type": "Trophy",
"name": "The champion"
},
{
"type": "Medal",
"name": "The runner up"
}
],
"address": {
"line1": "Brunel Building",
"line2": "57 North Wharf Road",
"line3": "",
"postcode": "W2 1HQ",
"county": "London"
},
"other": {
"concacaf": false,
"confederation": "UEFA",
"founder": null,
"random_array": [
"one",
2,
true,
false,
null
],
"random_json": {
"key_1": "value",
"key_2": 2,
"key_3": true,
"key_4": false,
"key_5": null
},
"uefa": true,
"world_ranking": 4
},
"plain": "{\"key_1\":\"value\",\"key_2\":2,\"key_3\":true,\"key_4\":false,\"key_5\":null}"
}

Structure


├── Makefile
├── client
│   ├── main.go
│   └── request.json
├── football
│   ├── client.go
│   └── server.go
├── go.mod
├── pkg
│   └── protobuf
│   └── football
│   ├── league
│   │   ├── address.pb.go
│   │   ├── address.proto
│   │   ├── award.pb.go
│   │   ├── award.proto
│   │   ├── league.pb.go
│   │   └── league.proto
│   ├── response.pb.go
│   ├── response.proto
│   ├── service.pb.go
│   └── service.proto
└── server
└── main.go

Files


Makefile


Run make compile to generate *.pb.go files.


.PHONY: compile
compile:
protoc --go_out=plugins=grpc:. --go_opt=paths=source_relative pkg/protobuf/football/league/*.proto
protoc --go_out=plugins=grpc:. --go_opt=paths=source_relative pkg/protobuf/football/*.proto

.PHONY: client
client:
go run --race client/main.go

.PHONY: server
server:
go run --race server/main.go

go.mod


module github.com/inanzzz/sport

go 1.15

require (
github.com/golang/protobuf v1.4.2
golang.org/x/net v0.0.0-20200904194848-62affa334b73 // indirect
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f // indirect
golang.org/x/text v0.3.3 // indirect
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d // indirect
google.golang.org/grpc v1.31.1
google.golang.org/protobuf v1.25.0
)

pkg/protobuf/football/league/address.proto


syntax = "proto3";

package football;

option go_package = "github.com/inanzzz/sport/pkg/protobuf/football/league";

message Address {
string line1 = 1;
string line2 = 2;
string line3 = 3;
string postcode = 4;
string county = 5;
}

pkg/protobuf/football/league/award.proto


syntax = "proto3";

package football;

option go_package = "github.com/inanzzz/sport/pkg/protobuf/football/league";

message Award {
enum Type {
None = 0;
Trophy = 1;
Medal = 2;
}

Type type = 1;
string name = 2;
}

pkg/protobuf/football/league/league.proto


syntax = "proto3";

package football;

option go_package = "github.com/inanzzz/sport/pkg/protobuf/football/league";

import "pkg/protobuf/football/league/address.proto";
import "pkg/protobuf/football/league/award.proto";

import "google/protobuf/struct.proto";

// You could import "well-known" google/protobuf/timestamp.proto package and
// replace string with google.protobuf.Timestamp type below for founded_at.

message CreateLeagueRequest {
string name = 1;
string founded_at = 2;
bool is_active = 3;
double budget = 4;
string mascot = 5;
map <string, string> stadiums = 6;
repeated string sponsors = 7;
repeated Award awards = 8;
Address address = 9;
google.protobuf.Struct other = 10; // an unknown set of json key/value pairs
google.protobuf.Value plain = 11; // an unknown json.RawMessage string
}

//message UpdateLeagueRequest {}
//message FindLeagueRequest {}
//message DeleteLeagueRequest {}

pkg/protobuf/football/response.proto


syntax = "proto3";

package football;

option go_package = "github.com/inanzzz/sport/pkg/protobuf/football";

// You could import "well-known" google/protobuf/any.proto package and
// replace bytes with google.protobuf.Any type below.

message Response {
enum Result {
SUCCESS = 0;
ERROR = 1;
}

message Success {
bytes data = 1;
}

message Error {
string message = 1;
bytes errors = 2;
}

Result result = 1;
Success success = 2;
Error error = 3;
}

pkg/protobuf/football/service.proto


syntax = "proto3";

package football;

option go_package = "github.com/inanzzz/sport/pkg/protobuf/football";

import "pkg/protobuf/football/response.proto";
import "pkg/protobuf/football/league/league.proto";

service FootballService {
rpc CreateLeague(CreateLeagueRequest) returns (Response) {}

// rpc UpdateLeague(UpdateLeagueRequest) returns (Response) {}
// rpc FindLeague(FindLeagueRequest) returns (Response) {}
// rpc DeleteLeague(DeleteLeagueRequest) returns (Response) {}
}

client/main.go


package main

import (
"bytes"
"context"
"io/ioutil"
"log"
"time"

"github.com/inanzzz/sport/football"
"google.golang.org/grpc"
)

func main() {
log.Println("client")

conn, err := grpc.Dial(":50051", grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
log.Fatalln(err)
}
defer conn.Close()

json, err := ioutil.ReadFile("client/request.json")
if err != nil {
log.Fatalln(err)
}

footballClient := football.NewClient(conn, time.Second)
err = footballClient.CreateLeague(context.Background(), bytes.NewBuffer(json))

log.Println("ERR:", err)
}

football/client.go


package football

import (
"context"
"fmt"
"io"
"log"
"time"

"github.com/inanzzz/sport/pkg/protobuf/football"
"github.com/inanzzz/sport/pkg/protobuf/football/league"

"github.com/golang/protobuf/jsonpb"
"google.golang.org/grpc"
"google.golang.org/grpc/status"
)

type Client struct {
footballClient football.FootballServiceClient
timeout time.Duration
}

func NewClient(conn grpc.ClientConnInterface, timeout time.Duration) Client {
return Client{
footballClient: football.NewFootballServiceClient(conn),
timeout: timeout,
}
}

func (c Client) CreateLeague(ctx context.Context, json io.Reader) error {
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(c.timeout))
defer cancel()

req := league.CreateLeagueRequest{}
if err := jsonpb.Unmarshal(json, &req); err != nil {
return fmt.Errorf("client create league: unmarshal: %w", err)
}

res, err := c.footballClient.CreateLeague(ctx, &req)
if err != nil {
if er, ok := status.FromError(err); ok {
return fmt.Errorf("client create league: code: %s - msg: %s", er.Code(), er.Message())
}
return fmt.Errorf("client create league: %w", err)
}

log.Println("RESULT:", res.Result)
log.Println("RESPONSE:", res)

return nil
}

server/main.go


package main

import (
"log"
"net"

"github.com/inanzzz/sport/football"
"google.golang.org/grpc"

protofootball "github.com/inanzzz/sport/pkg/protobuf/football"
)

func main() {
log.Println("server")

listener, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalln(err)
}

grpcServer := grpc.NewServer()
footballServer := football.NewServer()

protofootball.RegisterFootballServiceServer(grpcServer, footballServer)

log.Fatalln(grpcServer.Serve(listener))
}

football/server.go


package football

import (
"bytes"
"context"
"log"

"github.com/inanzzz/sport/pkg/protobuf/football"
"github.com/inanzzz/sport/pkg/protobuf/football/league"

"github.com/golang/protobuf/jsonpb"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

type Server struct {
football.UnimplementedFootballServiceServer
}

func NewServer() Server {
return Server{}
}

func (s Server) CreateLeague(ctx context.Context, req *league.CreateLeagueRequest) (*football.Response, error) {
json := bytes.Buffer{}
// OrigName uses the actual field names from the proto files rather than casting them to camelCase.
// EmitDefaults prevents discarding empty/nullable fields and keeps zero values.
mars := jsonpb.Marshaler{OrigName: true, EmitDefaults: true}
if err := mars.Marshal(&json, req); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "server create league: marshal: %v", err)
}

log.Println("REQUEST:", json.String())

return &football.Response{
Result: football.Response_SUCCESS,
Success: &football.Response_Success{
Data: []byte("good job"),
},
}, nil
}

Test


Server


make server

Client


make client

Results


When you send the JSON file shown at the beginning, server will output exactly the same JSON content. If you send either {} or all null values, the result should look like the one below.


# Request 1
{}

# Request 2
{
"name": null,
"founded_at": null,
"is_active": null,
"budget": null,
"mascot": null,
"stadiums": null,
"sponsors": null,
"awards": null,
"address": null,
"other": null,
"plain": null
}

As you can see, all the fields are set to their zero values.


# Result

{
"name": "",
"founded_at": "",
"is_active": false,
"budget": 0,
"mascot": "",
"stadiums": {

},
"sponsors": [

],
"awards": [

],
"address": null,
"other": null,
"plain": null
}

References