proto ファイルから、JSON フォーマットでやりとりする型定義ファイルを出力する protoc プラグインです。
- proto ファイルで言語を越えて型定義が出来るのはとても良い
- しかし protobuf ライブラリを入れるのが面倒
- 今のプロジェクトには既に JSON ライブラリが入っているので JSON でやり取りしたい
という時に使うプラグインです。
- C++ 用コードの出力 (Boost.JSON または nlohmann/json 利用)
- Unity 用コードの出力
- C 用コードの出力(コンパイルには C++ 用コードが必要)
- TypeScript 用コードの出力
- message, enum 対応
- repeated 対応
- oneof 対応
- optional 対応
- bytes 型の対応( protoc-gen-json-cpp のみ)
- オブジェクトの等値判定対応
- テスト
- 自動ビルド環境
- Flutter への対応
- C++, Unity, C 以外の言語への対応
- protoc-gen-json-unity の bytes 型の対応(いい方法求む)
- オブジェクトの大小の比較
- proto2 シンタックス対応
- map, any 型の対応
- service 定義の対応
- 実行速度の最適化(速度が欲しいならちゃんと protobuf 入れましょう)
まず、protobuf のリリース から、自身のプラットフォームの最新のバイナリをダウンロードして下さい。
Windows なら protoc-<version>-win64.zip
、macOS なら protoc-<version>-osx-x86_64.zip
などです。
ダウンロードが完了したら、これを展開し、protoc/bin
ディレクトリに環境変数 PATH
を通しておいて下さい。
次に、protoc-gen-jsonif のリリース から、protoc-gen-jsonif.tar.gz
をダウンロードして下さい。
ダウンロードが完了したら、これを展開し、自身のプラットフォームのディレクトリに環境変数 PATH
を通しておいて下さい。
Windows なら protoc-gen-jsonif/windows/amd64
、macOS なら protoc-gen-jsonif/macos/amd64
などです。
次に以下の内容を test.proto
として保存して下さい。
syntax = "proto3";
package test;
message Person {
string name = 1;
}
あとは以下のように実行して test.proto
ファイルを変換します。
mkdir -p out_cpp
protoc --jsonif-cpp_out=out_cpp/
これで out_cpp/
ディレクトリに C++ 用のファイルが出力されます。
また、
mkdir -p out_unity
protoc --jsonif-unity_out=out_unity/
こうすると out_unity/
ディレクトリに Unity 用のファイルが出力されます。
環境変数 PATH
を設定しなくても、以下のように指定すれば実行できます。
Windows 上で、ダウンロードした protoc が ./protoc
に、protoc-gen-jsonif が ./protoc-gen-jsonif
にあるという状態だとすると、
# C++
mkdir -p out_cpp
./protoc/bin/protoc.exe \
--plugin=protoc-gen-jsonif-cpp=./protoc-gen-jsonif/windows/amd64/protoc-gen-jsonif-cpp.exe \
--jsonif-cpp_out=out_cpp/ \
test.proto
# Unity
mkdir -p out_unity
./protoc/bin/protoc.exe \
--plugin=protoc-gen-jsonif-unity=./protoc-gen-jsonif/windows/amd64/protoc-gen-jsonif-unity.exe \
--jsonif-unity_out=out_unity/ \
test.proto
これで PATH
を設定しなくても変換できます。
例では全て、以下のような test.proto
ファイルがあるとしています。
syntax = "proto3";
package test;
message Person {
string name = 1;
}
message PersonList {
repeated Person people = 1;
}
C++ 用のファイルを出力するには以下のように利用します。
protoc --jsonif-cpp_out=out/ test.proto
こうすると C++ ファイルが自動生成されて、以下のように型定義を使って JSON 文字列をシリアライズ・デシリアライズ可能になります。
#include "test.json.h"
int main() {
test::PersonList p;
p.people.push_back(test::Person{"hoge"});
p.people.push_back(test::Person{"fuga"});
// JSON 文字列への変換
std::string str = jsonif::to_json(p);
std::cout << str << std::endl;
// → {"people":[{"name":"hoge"},{"name":"fuga"}]}
// JSON 文字列から元に戻す
p = jsonif::from_json<test::PersonList>(str);
std::cout << p.people[0].name << std::endl;
// → hoge
std::cout << p.people[1].name << std::endl;
// → fuga
}
自動生成された test.json.h
は以下のようになっています(若干変わっている可能性もあります)。
C++ 標準には JSON ライブラリが無いため、現在はデフォルトでは Boost.JSON を利用しています。
#include <string>
#include <vector>
#include <stddef.h>
#include <boost/json.hpp>
namespace test {
struct Person {
std::string name;
};
struct PersonList {
std::vector<::test::Person> people;
};
// ::test::Person
void tag_invoke(const boost::json::value_from_tag&, boost::json::value& jv, const ::test::Person& v) {
jv = {
{"name", boost::json::value_from(v.name)},
};
}
::test::Person tag_invoke(const boost::json::value_to_tag<::test::Person>&, const boost::json::value& jv) {
::test::Person v;
v.name = boost::json::value_to<std::string>(jv.at("name"));
return v;
}
// ::test::PersonList
void tag_invoke(const boost::json::value_from_tag&, boost::json::value& jv, const ::test::PersonList& v) {
jv = {
{"people", boost::json::value_from(v.people)},
};
}
::test::PersonList tag_invoke(const boost::json::value_to_tag<::test::PersonList>&, const boost::json::value& jv) {
::test::PersonList v;
v.people = boost::json::value_to<std::vector<::test::Person>>(jv.at("people"));
return v;
}
}
#ifndef JSONIF_HELPER_DEFINED
#define JSONIF_HELPER_DEFINED
namespace jsonif {
template<class T>
inline T from_json(const std::string& s) {
return boost::json::value_to<T>(boost::json::parse(s));
}
template<class T>
inline std::string to_json(const T& v) {
return boost::json::serialize(boost::json::value_from(v));
}
}
#endif
また、コンパイル時のフラグに JSONIF_USE_NLOHMANN_JSON
を指定すると、Boost.JSON の代わりに nlohmann/json を利用するようになります。
Unity 用のファイルを出力するには以下のように利用します。
protoc --jsonif-unity_out=out/ test.proto
こうすると Unity 用の C# ファイルが自動生成されて、以下のように JSON 文字列をシリアライズ・デシリアライズ可能になります。
void Start()
{
var p = new Test.PersonList();
p.people.Add(new Test.Person() { name = "hoge" });
p.people.Add(new Test.Person() { name = "fuga" });
// JSON 文字列への変換
string str = Jsonif.Json.ToJson(p);
Debug.Log(str);
// → {"people":[{"name":"hoge"},{"name":"fuga"}]}
// JSON 文字列から元に戻す
p = Jsonif.Json.FromJson<Test.PersonList>(str);
Debug.Log(p.people[0].name);
// → hoge
Debug.Log(p.people[1].name);
// → fuga
}
自動生成された Test.cs
と Jsonif.cs
は以下のようになっています(若干変わっている可能性もあります)。
Unity では内部的に JsonUtility を利用しています。
// Test.cs
namespace Test
{
[System.Serializable]
public class Person
{
public string name;
}
[System.Serializable]
public class PersonList
{
public global::Test.Person[] people;
}
}
// Jsonif.cs
using UnityEngine;
namespace Jsonif
{
public static class Json
{
public static string ToJson<T>(T v)
{
return JsonUtility.ToJson(v);
}
public static T FromJson<T>(string s)
{
return JsonUtility.FromJson<T>(s);
}
}
}
TypeScript 用のファイルを出力するには以下のように利用します。
protoc --jsonif-typescript_out=out/ test.proto
こうすると TypeScript 用のファイルが自動生成されて、以下のように JSON 文字列をシリアライズ・デシリアライズ可能になります。
import * as test from "./test";
import * as jsonif from "./jsonif";
var p = new test.PersonList();
p.people.push(new test.Person({name: "hoge"}));
p.people.push(new test.Person({name: "fuga"}));
// JSON 文字列への変換
const str = jsonif.toJson(p);
console.log(str)
// → {"people":[{"name":"hoge"},{"name":"fuga"}]}
// JSON 文字列から元に戻す
p = jsonif.fromJson<test.PersonList>(str, test.PersonList);
console.log(p.people[0].name);
// → hoge
console.log(p.people[1].name);
// → fuga
自動生成された test.ts
と jsonif.ts
は以下のようになっています(若干変わっている可能性もあります)。
TypeScript 版では内部的に JSON.parse() と JSON.stringify() を利用しています。
// test.ts
export type PersonObject = {
name?: string;
}
export class Person {
name: string = "";
constructor(obj: PersonObject = {}) {
if (obj.name !== undefined) {
this.name = obj.name;
}
}
getType(): typeof Person {
return Person;
}
static fromJson(json: string): Person {
return Person.fromObject(JSON.parse(json));
}
toJson(): string {
return JSON.stringify(this.toObject());
}
static fromObject(obj: PersonObject): Person {
return new Person(obj);
}
toObject(): PersonObject {
return {
name: this.name,
};
}
}
export type PersonListObject = {
people?: PersonObject[];
}
export class PersonList {
people: Person[] = [];
constructor(obj: PersonListObject = {}) {
if (obj.people !== undefined) {
this.people = obj.people.map((x) => Person.fromObject(x));
}
}
getType(): typeof PersonList {
return PersonList;
}
static fromJson(json: string): PersonList {
return PersonList.fromObject(JSON.parse(json));
}
toJson(): string {
return JSON.stringify(this.toObject());
}
static fromObject(obj: PersonListObject): PersonList {
return new PersonList(obj);
}
toObject(): PersonListObject {
return {
people: this.people.map((x) => x.toObject()),
};
}
}
// jsonif.ts
export interface Jsonif<T> {
getType: () => { fromJson(json: string): T };
toJson: () => string;
}
export function getType<T extends number | string | boolean | Jsonif<T>>(v: T): any {
if ((v as any).getType !== undefined) {
return (v as any).getType();
} else {
return v.constructor as any;
}
}
export function fromJson<T>(v: string, type: any): T {
if (type.fromJson !== undefined) {
return type.fromJson(v) as T;
} else {
return JSON.parse(v) as T;
}
}
export function toJson<T extends number | string | boolean | Jsonif<T>>(v: T): string {
if (typeof v === 'number' || typeof v === 'string' || typeof v === 'boolean') {
return JSON.stringify(v);
} else {
return v.toJson();
}
}
なお、TypeScript 版はパッケージの指定を無視します。 import 側で名前を指定して競合を避けて下さい。
A. JSON Interface の略です。
本当は protoc-gen-json という名前にしようと思ってたのですが、protoc-gen-json というのは既に存在 していて、これは proto ファイルそのものを JSON 化するものです。 これは欲しい機能ではなかったし、単に protoc-gen-json というとこっちを指すんだとするとちょっと違うので、Interface を付けて別の名前にしました。
A. protoc で生成するプラグインの命名ルールです。
protoc は、デフォルトでは --<NAME>_out=...
と指定したら protoc-gen-<NAME>
プログラムを実行してファイル生成を呼び出す仕組みになっています(--plugin
オプションで上書きできます)。
そのため protoc プラグインのリポジトリ名やバイナリ名に protoc-gen-
プリフィックスを付けるのが一般的です。
A. できません。
protoc-gen-jsonif は、protoc で定義した構造体同士でやり取りするために、内部のフォーマットとして JSON を利用しているだけです。 この内部フォーマットである JSON を外から利用されることは考えていないため、フィールド名の変更は不要だと考えています。
一応 C++ 版には jsonif_name
というフィールドオプションを定義していますが、これは C++ 版特有の機能であって、他の言語で対応する予定は無いです。
Copyright 2021 Wandbox LLC.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.