Repository: tableauio/tableau
Pull request #46 (2023-03-29 07:57:35)

feat: support custom field json name
- Add new field prop option: `json_name`, to support custom protobuf field json name, refer [google/protobuf/descriptor.proto FieldDescriptorProto#json_name](https://github.com/protocolbuffers/protobuf/blob/main/src/google/protobuf/descriptor.proto#L235).
- Add two tableauc config options:
  - option `UseProtoNames` uses proto field name instead of lowerCamelCase name in JSON field names.
  - option `UseEnumNumbers` emits enum values as numbers.

codecov[bot]: ## [Codecov](https://codecov.io/gh/tableauio/tableau/pull/46?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio) Report
> Merging [#46](https://codecov.io/gh/tableauio/tableau/pull/46?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio) (9806b7b) into [master](https://codecov.io/gh/tableauio/tableau/commit/bdff2153a54782e5c4ad2613f4dea7db01fcdb7e?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio) (bdff215) will **decrease** coverage by `0.02%`.
> The diff coverage is `45.16%`.

```diff
@@            Coverage Diff             @@
##           master      #46      +/-   ##
==========================================
- Coverage   30.23%   30.21%   -0.02%     
==========================================
  Files          25       25              
  Lines        3400     3422      +22     
==========================================
+ Hits         1028     1034       +6     
- Misses       2286     2302      +16     
  Partials       86       86              
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `30.21% <45.16%> (-0.02%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/tableauio/tableau/pull/46?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio) | Coverage Δ | |
|---|---|---|
| [internal/protogen/exporter.go](https://codecov.io/gh/tableauio/tableau/pull/46?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio#diff-aW50ZXJuYWwvcHJvdG9nZW4vZXhwb3J0ZXIuZ28=) | `0.00% <0.00%> (ø)` | |
| [internal/protogen/field\_prop.go](https://codecov.io/gh/tableauio/tableau/pull/46?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio#diff-aW50ZXJuYWwvcHJvdG9nZW4vZmllbGRfcHJvcC5nbw==) | `84.48% <100.00%> (+1.79%)` | :arrow_up: |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio)


Patch:
diff --git a/cmd/tableauc/main.go b/cmd/tableauc/main.go
index 500c6c9..22ba7e7 100644
--- a/cmd/tableauc/main.go
+++ b/cmd/tableauc/main.go
@@ -15,7 +15,7 @@ import (
 	"gopkg.in/yaml.v2"
 )
 
-const version = "0.5.6"
+const version = "0.5.7"
 const (
 	ModeDefault = "default" // generate both proto and conf files
 	ModeProto   = "proto"   // generate proto files only
diff --git a/format/format.go b/format/format.go
index 57449c5..78e5cb5 100644
--- a/format/format.go
+++ b/format/format.go
@@ -38,10 +38,10 @@ func Ext2Format(ext string) Format {
 		return XML
 	case JSONExt:
 		return JSON
-	case TextExt:
-		return Text
 	case BinExt:
 		return Bin
+	case TextExt:
+		return Text
 	default:
 		return UnknownFormat
 	}
@@ -57,10 +57,10 @@ func Format2Ext(fmt Format) string {
 		return XMLExt
 	case JSON:
 		return JSONExt
-	case Text:
-		return TextExt
 	case Bin:
 		return BinExt
+	case Text:
+		return TextExt
 	default:
 		return UnknownExt
 	}
diff --git a/internal/confgen/mexporter/mexporter.go b/internal/confgen/mexporter/mexporter.go
index 27a61f9..30661c0 100644
--- a/internal/confgen/mexporter/mexporter.go
+++ b/internal/confgen/mexporter/mexporter.go
@@ -90,12 +90,13 @@ func (x *messageExporter) export(fmt format.Format) error {
 }
 
 func (x *messageExporter) marshalToJSON() (out []byte, err error) {
-	emitUnpopulated := x.outputOpt.EmitUnpopulated
 	if x.outputOpt.Pretty {
 		opts := protojson.MarshalOptions{
 			Multiline:       true,
 			Indent:          "    ",
-			EmitUnpopulated: emitUnpopulated,
+			EmitUnpopulated: x.outputOpt.EmitUnpopulated,
+			UseProtoNames:   x.outputOpt.UseProtoNames,
+			UseEnumNumbers:  x.outputOpt.UseEnumNumbers,
 		}
 		return opts.Marshal(x.msg)
 	}
diff --git a/internal/confgen/version.go b/internal/confgen/version.go
index f737ed8..7c88071 100644
--- a/internal/confgen/version.go
+++ b/internal/confgen/version.go
@@ -4,7 +4,7 @@ import "fmt"
 
 const (
 	App     = "confgen"
-	Version = "0.4.2"
+	Version = "0.5.0"
 )
 
 func AppVersion() string {
diff --git a/internal/protogen/exporter.go b/internal/protogen/exporter.go
index 2ec8d11..ddd330e 100644
--- a/internal/protogen/exporter.go
+++ b/internal/protogen/exporter.go
@@ -160,7 +160,8 @@ func (x *sheetExporter) exportField(depth int, tagid int, field *tableaupb.Field
 	if x.ws.Options != nil && x.ws.Options.FieldPresence && types.IsScalarType(field.FullType) {
 		label = "optional "
 	}
-	x.g.P(printer.Indent(depth), label, field.FullType, " ", field.Name, " = ", tagid, " [(tableau.field) = {", marshalToText(field.Options), "}];")
+
+	x.g.P(printer.Indent(depth), label, field.FullType, " ", field.Name, " = ", tagid, " ", genFieldOptionsString(field.Options), ";")
 
 	// if field.FullType == "google.protobuf.Timestamp" {
 	// 	x.Imports[timestampProtoPath] = true
@@ -214,6 +215,28 @@ func (x *sheetExporter) exportField(depth int, tagid int, field *tableaupb.Field
 	return nil
 }
 
+func genFieldOptionsString(opts *tableaupb.FieldOptions) string {
+	jsonName := ""
+	// remember and then clear protobuf built-in options
+	if opts.Prop != nil {
+		jsonName = opts.Prop.JsonName
+		opts.Prop.JsonName = ""
+
+		// set nil if field prop is empty
+		if IsEmptyFieldProp(opts.Prop) {
+			opts.Prop = nil
+		}
+	}
+
+	// compose this field options
+	fieldOpts := "[(tableau.field) = {" + marshalToText(opts) + "}"
+	if jsonName != "" {
+		fieldOpts += `, json_name="` + jsonName + `"`
+	}
+	fieldOpts += "]"
+	return fieldOpts
+}
+
 func marshalToText(m protoreflect.ProtoMessage) string {
 	// text := proto.CompactTextString(field.Options)
 	bin, err := prototext.Marshal(m)
diff --git a/internal/protogen/field_prop.go b/internal/protogen/field_prop.go
index e9ddd15..c5070f0 100644
--- a/internal/protogen/field_prop.go
+++ b/internal/protogen/field_prop.go
@@ -7,18 +7,23 @@ import (
 
 var emptyFieldProp = &tableaupb.FieldProp{}
 
+func IsEmptyFieldProp(prop *tableaupb.FieldProp) bool {
+	return proto.Equal(emptyFieldProp, prop)
+}
+
 // ExtractMapFieldProp extracts the specified props which the map field recognizes.
 func ExtractMapFieldProp(prop *tableaupb.FieldProp) *tableaupb.FieldProp {
 	if prop == nil {
 		return nil
 	}
 	p := &tableaupb.FieldProp{
+		JsonName: prop.JsonName,
 		Unique:   prop.Unique,
 		Sequence: prop.Sequence,
 		Fixed:    prop.Fixed,
 		Size:     prop.Size,
 	}
-	if proto.Equal(emptyFieldProp, p) {
+	if IsEmptyFieldProp(p) {
 		return nil
 	}
 	return p
@@ -30,12 +35,13 @@ func ExtractListFieldProp(prop *tableaupb.FieldProp) *tableaupb.FieldProp {
 		return nil
 	}
 	p := &tableaupb.FieldProp{
+		JsonName: prop.JsonName,
 		Unique:   prop.Unique, // only for keyed list ?
 		Sequence: prop.Sequence,
 		Fixed:    prop.Fixed,
 		Size:     prop.Size,
 	}
-	if proto.Equal(emptyFieldProp, p) {
+	if IsEmptyFieldProp(p) {
 		return nil
 	}
 	return p
@@ -47,9 +53,10 @@ func ExtractStructFieldProp(prop *tableaupb.FieldProp) *tableaupb.FieldProp {
 		return nil
 	}
 	p := &tableaupb.FieldProp{
-		Form: prop.Form,
+		JsonName: prop.JsonName,
+		Form:     prop.Form,
 	}
-	if proto.Equal(emptyFieldProp, p) {
+	if IsEmptyFieldProp(p) {
 		return nil
 	}
 	return p
@@ -61,11 +68,12 @@ func ExtractScalarFieldProp(prop *tableaupb.FieldProp) *tableaupb.FieldProp {
 		return nil
 	}
 	p := &tableaupb.FieldProp{
-		Range:   prop.Range,
-		Refer:   prop.Refer,
-		Default: prop.Default,
+		JsonName: prop.JsonName,
+		Range:    prop.Range,
+		Refer:    prop.Refer,
+		Default:  prop.Default,
 	}
-	if proto.Equal(emptyFieldProp, p) {
+	if IsEmptyFieldProp(p) {
 		return nil
 	}
 	return p
diff --git a/internal/protogen/version.go b/internal/protogen/version.go
index ba97d2e..20a6cba 100644
--- a/internal/protogen/version.go
+++ b/internal/protogen/version.go
@@ -4,7 +4,7 @@ import "fmt"
 
 const (
 	App     = "protogen"
-	Version = "0.4.3"
+	Version = "0.5.0"
 )
 
 func AppVersion() string {
diff --git a/options/options.go b/options/options.go
index bcc6a8e..dd01224 100644
--- a/options/options.go
+++ b/options/options.go
@@ -35,14 +35,17 @@ type HeaderOption struct {
 	//
 	// Default: 1.
 	Namerow int32
+
 	// Exact row number of column type definition at a worksheet.
 	//
 	// Default: 2.
 	Typerow int32
+
 	// Exact row number of column note at a worksheet.
 	//
 	// Default: 3.
 	Noterow int32
+
 	// Start row number of data at a worksheet.
 	//
 	// Default: 4.
@@ -53,6 +56,7 @@ type HeaderOption struct {
 	//
 	// Default: 0.
 	Nameline int32
+
 	// The line number of column type definition in a cell.
 	// Value 0 means the whole cell.
 	//
@@ -64,6 +68,7 @@ type HeaderOption struct {
 type ProtoOption struct {
 	// Input options for generating proto files.
 	Input *ProtoInputOption `yaml:"input"`
+
 	// Output options for generating proto files.
 	Output *ProtoOutputOption `yaml:"output"`
 }
@@ -72,35 +77,42 @@ type ProtoOption struct {
 type ProtoInputOption struct {
 	// Header options of worksheet.
 	Header *HeaderOption `yaml:"header"`
+
 	// The proto paths are used to search for dependencies that are referenced in import
 	// statements in proto source files. If no import paths are provided then
 	// "." (current directory) is assumed to be the only import path.
 	//
 	// Default: nil.
 	ProtoPaths []string `yaml:"protoPaths"`
+
 	// The enums and messages in ProtoFiles can be used in Excel/CSV/XML as
 	// common types.
 	//
 	// Default: nil.
 	ProtoFiles []string `yaml:"protoFiles"`
+
 	// Specify input file formats.
 	// Note: recognize all formats (Excel/CSV/XML) if not set (value is nil).
 	//
 	// Default: nil.
+
 	Formats []format.Format `yaml:"formats"`
 	// Specify only these subdirs (relative to input dir) to be processed.
 	//
 	// Default: nil.
 	Subdirs []string `yaml:"subdirs"`
+
 	// Specify rewrite subdir path (relative to input dir).
 	//
 	// Default: nil.
 	SubdirRewrites map[string]string `yaml:"subdirRewrites"`
+
 	// Follow the symbolic links when traversing directories recursively.
 	// WARN: be careful to use this option, it may lead to infinite loop.
 	//
 	// Default: false.
 	FollowSymlink bool `yaml:"followSymlink"`
+
 	// Specify metasheet name. Metasheet is "@TABLEAU" if not set.
 	//
 	// Default: "".
@@ -113,10 +125,12 @@ type ProtoOutputOption struct {
 	//
 	// Default: "".
 	Subdir string `yaml:"subdir"`
+
 	// Dir separator `/` or `\`  in filename is replaced by "__".
 	//
 	// Default: false.
 	FilenameWithSubdirPrefix bool `yaml:"filenameWithSubdirPrefix"`
+
 	// Append suffix to each generated proto filename.
 	//
 	// Default: "".
@@ -133,6 +147,7 @@ type ProtoOutputOption struct {
 type ConfOption struct {
 	// Input options for generating conf files.
 	Input *ConfInputOption `yaml:"input"`
+
 	// Output options for generating conf files.
 	Output *ConfOutputOption `yaml:"output"`
 }
@@ -145,6 +160,7 @@ type ConfInputOption struct {
 	//
 	// Default: nil.
 	ProtoPaths []string `yaml:"protoPaths"`
+
 	// The files to be parsed to generate configurations.
 	//
 	// NOTE:
@@ -154,6 +170,7 @@ type ConfInputOption struct {
 	//
 	// Default: nil.
 	ProtoFiles []string `yaml:"protoFiles"`
+
 	// The files not to be parsed to generate configurations.
 	//
 	// NOTE: Glob patterns are supported, which can specify sets
@@ -161,15 +178,18 @@ type ConfInputOption struct {
 	//
 	// Default: nil.
 	ExcludedProtoFiles []string `yaml:"excludedProtoFiles"`
+
 	// Specify input file formats to be parsed.
 	// Note: recognize all formats (Excel/CSV/XML) if not set (value is nil).
 	//
 	// Default: nil.
 	Formats []format.Format
+
 	// Specify only these subdirs (relative to workbook name option in proto file).
 	//
 	// Default: nil.
 	Subdirs []string
+
 	// Specify rewrite subdir path (relative to workbook name option in proto file).
 	//
 	// Default: nil.
@@ -182,15 +202,18 @@ type ConfOutputOption struct {
 	//
 	// Default: "".
 	Subdir string `yaml:"subdir"`
+
 	// Specify generated conf file formats. If not set, it will generate all
 	// formats (JSON/Text/Bin).
 	//
 	// Default: nil.
 	Formats []format.Format
+
 	// Output pretty format of JSON, with multiline and indent.
 	//
 	// Default: false.
 	Pretty bool
+
 	// EmitUnpopulated specifies whether to emit unpopulated fields. It does not
 	// emit unpopulated oneof fields or unpopulated extension fields.
 	// The JSON value emitted for unpopulated fields are as follows:
@@ -212,6 +235,13 @@ type ConfOutputOption struct {
 	//
 	// Default: false.
 	EmitUnpopulated bool `yaml:"emitUnpopulated"`
+
+	// UseProtoNames uses proto field name instead of lowerCamelCase name in JSON
+	// field names.
+	UseProtoNames bool `yaml:"useProtoNames"`
+
+	// UseEnumNumbers emits enum values as numbers.
+	UseEnumNumbers bool `yaml:"useEnumNumbers"`
 }
 
 // Option is the functional option type.
diff --git a/proto/tableau/protobuf/tableau.proto b/proto/tableau/protobuf/tableau.proto
index af12a05..992ee4f 100644
--- a/proto/tableau/protobuf/tableau.proto
+++ b/proto/tableau/protobuf/tableau.proto
@@ -187,6 +187,11 @@ message FieldProp {
   uint32 size = 7;
   // Specify cell data form for parsing.
   Form form = 8;
+  // JSON name of this field. The value is set by protocol compiler. If the
+  // user has set a "json_name" option on this field, that option's value
+  // will be used. Otherwise, it's deduced from the field's name by converting
+  // it to camelCase.
+  string json_name = 9;
 }
 
 // Layout of list and map.
diff --git a/proto/tableaupb/tableau.pb.go b/proto/tableaupb/tableau.pb.go
index f662a77..b855606 100644
--- a/proto/tableaupb/tableau.pb.go
+++ b/proto/tableaupb/tableau.pb.go
@@ -844,6 +844,11 @@ type FieldProp struct {
 	Size uint32 `protobuf:"varint,7,opt,name=size,proto3" json:"size,omitempty"`
 	// Specify cell data form for parsing.
 	Form Form `protobuf:"varint,8,opt,name=form,proto3,enum=tableau.Form" json:"form,omitempty"`
+	// JSON name of this field. The value is set by protocol compiler. If the
+	// user has set a "json_name" option on this field, that option's value
+	// will be used. Otherwise, it's deduced from the field's name by converting
+	// it to camelCase.
+	JsonName string `protobuf:"bytes,9,opt,name=json_name,json=jsonName,proto3" json:"json_name,omitempty"`
 }
 
 func (x *FieldProp) Reset() {
@@ -934,6 +939,13 @@ func (x *FieldProp) GetForm() Form {
 	return Form_FORM_DEFAULT
 }
 
+func (x *FieldProp) GetJsonName() string {
+	if x != nil {
+		return x.JsonName
+	}
+	return ""
+}
+
 var file_tableau_protobuf_tableau_proto_extTypes = []protoimpl.ExtensionInfo{
 	{
 		ExtendedType:  (*descriptorpb.FileOptions)(nil),
@@ -1100,7 +1112,7 @@ var file_tableau_protobuf_tableau_proto_rawDesc = []byte{
 	0x6e, 0x61, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x6f, 0x70, 0x74, 0x69, 0x6f,
 	0x6e, 0x61, 0x6c, 0x12, 0x26, 0x0a, 0x04, 0x70, 0x72, 0x6f, 0x70, 0x18, 0x0f, 0x20, 0x01, 0x28,
 	0x0b, 0x32, 0x12, 0x2e, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x61, 0x75, 0x2e, 0x46, 0x69, 0x65, 0x6c,
-	0x64, 0x50, 0x72, 0x6f, 0x70, 0x52, 0x04, 0x70, 0x72, 0x6f, 0x70, 0x22, 0xe4, 0x01, 0x0a, 0x09,
+	0x64, 0x50, 0x72, 0x6f, 0x70, 0x52, 0x04, 0x70, 0x72, 0x6f, 0x70, 0x22, 0x81, 0x02, 0x0a, 0x09,
 	0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x72, 0x6f, 0x70, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x61, 0x6e,
 	0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x12,
 	0x16, 0x0a, 0x06, 0x75, 0x6e, 0x69, 0x71, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52,
@@ -1114,70 +1126,72 @@ var file_tableau_protobuf_tableau_proto_rawDesc = []byte{
 	0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x73, 0x69,
 	0x7a, 0x65, 0x12, 0x21, 0x0a, 0x04, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0e,
 	0x32, 0x0d, 0x2e, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x61, 0x75, 0x2e, 0x46, 0x6f, 0x72, 0x6d, 0x52,
-	0x04, 0x66, 0x6f, 0x72, 0x6d, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e,
-	0x63, 0x65, 0x2a, 0x5b, 0x0a, 0x06, 0x4c, 0x61, 0x79, 0x6f, 0x75, 0x74, 0x12, 0x12, 0x0a, 0x0e,
-	0x4c, 0x41, 0x59, 0x4f, 0x55, 0x54, 0x5f, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10, 0x00,
-	0x12, 0x13, 0x0a, 0x0f, 0x4c, 0x41, 0x59, 0x4f, 0x55, 0x54, 0x5f, 0x56, 0x45, 0x52, 0x54, 0x49,
-	0x43, 0x41, 0x4c, 0x10, 0x01, 0x12, 0x15, 0x0a, 0x11, 0x4c, 0x41, 0x59, 0x4f, 0x55, 0x54, 0x5f,
-	0x48, 0x4f, 0x52, 0x49, 0x5a, 0x4f, 0x4e, 0x54, 0x41, 0x4c, 0x10, 0x02, 0x12, 0x11, 0x0a, 0x0d,
-	0x4c, 0x41, 0x59, 0x4f, 0x55, 0x54, 0x5f, 0x49, 0x4e, 0x43, 0x45, 0x4c, 0x4c, 0x10, 0x03, 0x2a,
-	0x42, 0x0a, 0x04, 0x53, 0x70, 0x61, 0x6e, 0x12, 0x10, 0x0a, 0x0c, 0x53, 0x50, 0x41, 0x4e, 0x5f,
-	0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10, 0x00, 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x50, 0x41,
-	0x4e, 0x5f, 0x43, 0x52, 0x4f, 0x53, 0x53, 0x5f, 0x43, 0x45, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x13,
-	0x0a, 0x0f, 0x53, 0x50, 0x41, 0x4e, 0x5f, 0x49, 0x4e, 0x4e, 0x45, 0x52, 0x5f, 0x43, 0x45, 0x4c,
-	0x4c, 0x10, 0x02, 0x2a, 0x64, 0x0a, 0x04, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x10, 0x0a, 0x0c, 0x4d,
-	0x4f, 0x44, 0x45, 0x5f, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10, 0x00, 0x12, 0x0f, 0x0a,
-	0x0b, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x55, 0x45, 0x5f, 0x43, 0x53, 0x56, 0x10, 0x01, 0x12, 0x10,
-	0x0a, 0x0c, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x55, 0x45, 0x5f, 0x4a, 0x53, 0x4f, 0x4e, 0x10, 0x02,
-	0x12, 0x12, 0x0a, 0x0e, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x45, 0x4e, 0x55, 0x4d, 0x5f, 0x54, 0x59,
-	0x50, 0x45, 0x10, 0x03, 0x12, 0x13, 0x0a, 0x0f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x55, 0x4e, 0x49,
-	0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x10, 0x04, 0x2a, 0x36, 0x0a, 0x04, 0x46, 0x6f, 0x72,
-	0x6d, 0x12, 0x10, 0x0a, 0x0c, 0x46, 0x4f, 0x52, 0x4d, 0x5f, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c,
-	0x54, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x46, 0x4f, 0x52, 0x4d, 0x5f, 0x54, 0x45, 0x58, 0x54,
-	0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, 0x46, 0x4f, 0x52, 0x4d, 0x5f, 0x4a, 0x53, 0x4f, 0x4e, 0x10,
-	0x02, 0x3a, 0x54, 0x0a, 0x08, 0x77, 0x6f, 0x72, 0x6b, 0x62, 0x6f, 0x6f, 0x6b, 0x12, 0x1c, 0x2e,
+	0x04, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x1b, 0x0a, 0x09, 0x6a, 0x73, 0x6f, 0x6e, 0x5f, 0x6e, 0x61,
+	0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6a, 0x73, 0x6f, 0x6e, 0x4e, 0x61,
+	0x6d, 0x65, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x2a,
+	0x5b, 0x0a, 0x06, 0x4c, 0x61, 0x79, 0x6f, 0x75, 0x74, 0x12, 0x12, 0x0a, 0x0e, 0x4c, 0x41, 0x59,
+	0x4f, 0x55, 0x54, 0x5f, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10, 0x00, 0x12, 0x13, 0x0a,
+	0x0f, 0x4c, 0x41, 0x59, 0x4f, 0x55, 0x54, 0x5f, 0x56, 0x45, 0x52, 0x54, 0x49, 0x43, 0x41, 0x4c,
+	0x10, 0x01, 0x12, 0x15, 0x0a, 0x11, 0x4c, 0x41, 0x59, 0x4f, 0x55, 0x54, 0x5f, 0x48, 0x4f, 0x52,
+	0x49, 0x5a, 0x4f, 0x4e, 0x54, 0x41, 0x4c, 0x10, 0x02, 0x12, 0x11, 0x0a, 0x0d, 0x4c, 0x41, 0x59,
+	0x4f, 0x55, 0x54, 0x5f, 0x49, 0x4e, 0x43, 0x45, 0x4c, 0x4c, 0x10, 0x03, 0x2a, 0x42, 0x0a, 0x04,
+	0x53, 0x70, 0x61, 0x6e, 0x12, 0x10, 0x0a, 0x0c, 0x53, 0x50, 0x41, 0x4e, 0x5f, 0x44, 0x45, 0x46,
+	0x41, 0x55, 0x4c, 0x54, 0x10, 0x00, 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x50, 0x41, 0x4e, 0x5f, 0x43,
+	0x52, 0x4f, 0x53, 0x53, 0x5f, 0x43, 0x45, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x53,
+	0x50, 0x41, 0x4e, 0x5f, 0x49, 0x4e, 0x4e, 0x45, 0x52, 0x5f, 0x43, 0x45, 0x4c, 0x4c, 0x10, 0x02,
+	0x2a, 0x64, 0x0a, 0x04, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x10, 0x0a, 0x0c, 0x4d, 0x4f, 0x44, 0x45,
+	0x5f, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10, 0x00, 0x12, 0x0f, 0x0a, 0x0b, 0x4d, 0x4f,
+	0x44, 0x45, 0x5f, 0x55, 0x45, 0x5f, 0x43, 0x53, 0x56, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x4d,
+	0x4f, 0x44, 0x45, 0x5f, 0x55, 0x45, 0x5f, 0x4a, 0x53, 0x4f, 0x4e, 0x10, 0x02, 0x12, 0x12, 0x0a,
+	0x0e, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x45, 0x4e, 0x55, 0x4d, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x10,
+	0x03, 0x12, 0x13, 0x0a, 0x0f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x55, 0x4e, 0x49, 0x4f, 0x4e, 0x5f,
+	0x54, 0x59, 0x50, 0x45, 0x10, 0x04, 0x2a, 0x36, 0x0a, 0x04, 0x46, 0x6f, 0x72, 0x6d, 0x12, 0x10,
+	0x0a, 0x0c, 0x46, 0x4f, 0x52, 0x4d, 0x5f, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10, 0x00,
+	0x12, 0x0d, 0x0a, 0x09, 0x46, 0x4f, 0x52, 0x4d, 0x5f, 0x54, 0x45, 0x58, 0x54, 0x10, 0x01, 0x12,
+	0x0d, 0x0a, 0x09, 0x46, 0x4f, 0x52, 0x4d, 0x5f, 0x4a, 0x53, 0x4f, 0x4e, 0x10, 0x02, 0x3a, 0x54,
+	0x0a, 0x08, 0x77, 0x6f, 0x72, 0x6b, 0x62, 0x6f, 0x6f, 0x6b, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f,
+	0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x46, 0x69, 0x6c,
+	0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xd0, 0x86, 0x03, 0x20, 0x01, 0x28, 0x0b,
+	0x32, 0x18, 0x2e, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x61, 0x75, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x62,
+	0x6f, 0x6f, 0x6b, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x08, 0x77, 0x6f, 0x72, 0x6b,
+	0x62, 0x6f, 0x6f, 0x6b, 0x3a, 0x5a, 0x0a, 0x09, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x68, 0x65, 0x65,
+	0x74, 0x12, 0x1f, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+	0x62, 0x75, 0x66, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f,
+	0x6e, 0x73, 0x18, 0xd0, 0x86, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x74, 0x61, 0x62,
+	0x6c, 0x65, 0x61, 0x75, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x68, 0x65, 0x65, 0x74, 0x4f, 0x70,
+	0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x09, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x68, 0x65, 0x65, 0x74,
+	0x3a, 0x37, 0x0a, 0x05, 0x75, 0x6e, 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
+	0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4d, 0x65, 0x73, 0x73,
+	0x61, 0x67, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xd1, 0x86, 0x03, 0x20, 0x01,
+	0x28, 0x08, 0x52, 0x05, 0x75, 0x6e, 0x69, 0x6f, 0x6e, 0x3a, 0x4c, 0x0a, 0x05, 0x66, 0x69, 0x65,
+	0x6c, 0x64, 0x12, 0x1d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
+	0x6f, 0x62, 0x75, 0x66, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e,
+	0x73, 0x18, 0xd0, 0x86, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x61, 0x62, 0x6c,
+	0x65, 0x61, 0x75, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73,
+	0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x3a, 0x4a, 0x0a, 0x05, 0x65, 0x74, 0x79, 0x70, 0x65,
+	0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
+	0x75, 0x66, 0x2e, 0x45, 0x6e, 0x75, 0x6d, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xd0,
+	0x86, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x61, 0x75,
+	0x2e, 0x45, 0x6e, 0x75, 0x6d, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x05, 0x65, 0x74,
+	0x79, 0x70, 0x65, 0x3a, 0x56, 0x0a, 0x06, 0x65, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x21, 0x2e,
 	0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
-	0x46, 0x69, 0x6c, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xd0, 0x86, 0x03, 0x20,
-	0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x61, 0x75, 0x2e, 0x57, 0x6f,
-	0x72, 0x6b, 0x62, 0x6f, 0x6f, 0x6b, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x08, 0x77,
-	0x6f, 0x72, 0x6b, 0x62, 0x6f, 0x6f, 0x6b, 0x3a, 0x5a, 0x0a, 0x09, 0x77, 0x6f, 0x72, 0x6b, 0x73,
-	0x68, 0x65, 0x65, 0x74, 0x12, 0x1f, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72,
-	0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x4f, 0x70,
-	0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xd0, 0x86, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e,
-	0x74, 0x61, 0x62, 0x6c, 0x65, 0x61, 0x75, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x68, 0x65, 0x65,
-	0x74, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x09, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x68,
-	0x65, 0x65, 0x74, 0x3a, 0x37, 0x0a, 0x05, 0x75, 0x6e, 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x2e, 0x67,
-	0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4d,
-	0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xd1, 0x86,
-	0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x75, 0x6e, 0x69, 0x6f, 0x6e, 0x3a, 0x4c, 0x0a, 0x05,
-	0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x1d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
-	0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4f, 0x70, 0x74,
-	0x69, 0x6f, 0x6e, 0x73, 0x18, 0xd0, 0x86, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74,
-	0x61, 0x62, 0x6c, 0x65, 0x61, 0x75, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4f, 0x70, 0x74, 0x69,
-	0x6f, 0x6e, 0x73, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x3a, 0x4a, 0x0a, 0x05, 0x65, 0x74,
-	0x79, 0x70, 0x65, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
-	0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6e, 0x75, 0x6d, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e,
-	0x73, 0x18, 0xd0, 0x86, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x74, 0x61, 0x62, 0x6c,
-	0x65, 0x61, 0x75, 0x2e, 0x45, 0x6e, 0x75, 0x6d, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52,
-	0x05, 0x65, 0x74, 0x79, 0x70, 0x65, 0x3a, 0x56, 0x0a, 0x06, 0x65, 0x76, 0x61, 0x6c, 0x75, 0x65,
-	0x12, 0x21, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
-	0x75, 0x66, 0x2e, 0x45, 0x6e, 0x75, 0x6d, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4f, 0x70, 0x74, 0x69,
-	0x6f, 0x6e, 0x73, 0x18, 0xd0, 0x86, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x74, 0x61,
-	0x62, 0x6c, 0x65, 0x61, 0x75, 0x2e, 0x45, 0x6e, 0x75, 0x6d, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4f,
-	0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x06, 0x65, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x4f,
-	0x0a, 0x05, 0x6f, 0x6e, 0x65, 0x6f, 0x66, 0x12, 0x1d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
-	0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4f, 0x6e, 0x65, 0x6f, 0x66, 0x4f,
-	0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xd0, 0x86, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15,
-	0x2e, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x61, 0x75, 0x2e, 0x4f, 0x6e, 0x65, 0x6f, 0x66, 0x4f, 0x70,
-	0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x05, 0x6f, 0x6e, 0x65, 0x6f, 0x66, 0x88, 0x01, 0x01, 0x42,
-	0x75, 0x0a, 0x14, 0x63, 0x6f, 0x6d, 0x2e, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x61, 0x75, 0x2e, 0x70,
-	0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x42, 0x0c, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x61, 0x75,
-	0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e,
-	0x63, 0x6f, 0x6d, 0x2f, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x61, 0x75, 0x69, 0x6f, 0x2f, 0x74, 0x61,
-	0x62, 0x6c, 0x65, 0x61, 0x75, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x74, 0x61, 0x62, 0x6c,
-	0x65, 0x61, 0x75, 0x70, 0x62, 0xa2, 0x02, 0x03, 0x54, 0x50, 0x42, 0xaa, 0x02, 0x18, 0x54, 0x61,
-	0x62, 0x6c, 0x65, 0x61, 0x75, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54,
-	0x61, 0x62, 0x6c, 0x65, 0x61, 0x75, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x45, 0x6e, 0x75, 0x6d, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73,
+	0x18, 0xd0, 0x86, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x74, 0x61, 0x62, 0x6c, 0x65,
+	0x61, 0x75, 0x2e, 0x45, 0x6e, 0x75, 0x6d, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4f, 0x70, 0x74, 0x69,
+	0x6f, 0x6e, 0x73, 0x52, 0x06, 0x65, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x4f, 0x0a, 0x05, 0x6f,
+	0x6e, 0x65, 0x6f, 0x66, 0x12, 0x1d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72,
+	0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4f, 0x6e, 0x65, 0x6f, 0x66, 0x4f, 0x70, 0x74, 0x69,
+	0x6f, 0x6e, 0x73, 0x18, 0xd0, 0x86, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x61,
+	0x62, 0x6c, 0x65, 0x61, 0x75, 0x2e, 0x4f, 0x6e, 0x65, 0x6f, 0x66, 0x4f, 0x70, 0x74, 0x69, 0x6f,
+	0x6e, 0x73, 0x52, 0x05, 0x6f, 0x6e, 0x65, 0x6f, 0x66, 0x88, 0x01, 0x01, 0x42, 0x75, 0x0a, 0x14,
+	0x63, 0x6f, 0x6d, 0x2e, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x61, 0x75, 0x2e, 0x70, 0x72, 0x6f, 0x74,
+	0x6f, 0x62, 0x75, 0x66, 0x42, 0x0c, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x61, 0x75, 0x50, 0x72, 0x6f,
+	0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
+	0x2f, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x61, 0x75, 0x69, 0x6f, 0x2f, 0x74, 0x61, 0x62, 0x6c, 0x65,
+	0x61, 0x75, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x61, 0x75,
+	0x70, 0x62, 0xa2, 0x02, 0x03, 0x54, 0x50, 0x42, 0xaa, 0x02, 0x18, 0x54, 0x61, 0x62, 0x6c, 0x65,
+	0x61, 0x75, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x61, 0x62, 0x6c,
+	0x65, 0x61, 0x75, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (


Test patch:
diff --git a/internal/protogen/exporter_test.go b/internal/protogen/exporter_test.go
new file mode 100644
index 0000000..86e3fad
--- /dev/null
+++ b/internal/protogen/exporter_test.go
@@ -0,0 +1,72 @@
+package protogen
+
+import (
+	"testing"
+
+	"github.com/tableauio/tableau/proto/tableaupb"
+)
+
+func Test_genFieldOptionsString(t *testing.T) {
+	type args struct {
+		opts *tableaupb.FieldOptions
+	}
+	tests := []struct {
+		name string
+		args args
+		want string
+	}{
+		{
+			name: "only-name",
+			args: args{
+				opts: &tableaupb.FieldOptions{
+					Name: "ItemID",
+				},
+			},
+			want: `[(tableau.field) = {name:"ItemID"}]`,
+		},
+		{
+			name: "name-and-prop",
+			args: args{
+				opts: &tableaupb.FieldOptions{
+					Name: "ItemID",
+					Prop: &tableaupb.FieldProp{
+						Unique: true,
+					},
+				},
+			},
+			want: `[(tableau.field) = {name:"ItemID" prop:{unique:true}}]`,
+		},
+		{
+			name: "name-prop-and-json-name",
+			args: args{
+				opts: &tableaupb.FieldOptions{
+					Name: "ItemID",
+					Prop: &tableaupb.FieldProp{
+						Unique:   true,
+						JsonName: "item_id_1",
+					},
+				},
+			},
+			want: `[(tableau.field) = {name:"ItemID" prop:{unique:true}}, json_name="item_id_1"]`,
+		},
+		{
+			name: "name-and-prop-json_name",
+			args: args{
+				opts: &tableaupb.FieldOptions{
+					Name: "ItemID",
+					Prop: &tableaupb.FieldProp{
+						JsonName: "item_id_1",
+					},
+				},
+			},
+			want: `[(tableau.field) = {name:"ItemID"}, json_name="item_id_1"]`,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := genFieldOptionsString(tt.args.opts); got != tt.want {
+				t.Errorf("genFieldOptionsString() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/internal/protogen/field_prop_test.go b/internal/protogen/field_prop_test.go
index 497bbd8..c833eaf 100644
--- a/internal/protogen/field_prop_test.go
+++ b/internal/protogen/field_prop_test.go
@@ -7,6 +7,48 @@ import (
 	"google.golang.org/protobuf/proto"
 )
 
+func TestIsEmptyFieldProp(t *testing.T) {
+	type args struct {
+		prop *tableaupb.FieldProp
+	}
+	tests := []struct {
+		name string
+		args args
+		want bool
+	}{
+		{
+			name: "nilMapFieldProp",
+			args: args{
+				prop: nil,
+			},
+			want: false,
+		},
+		{
+			name: "emptyMapFieldProp",
+			args: args{
+				prop: &tableaupb.FieldProp{},
+			},
+			want: true,
+		},
+		{
+			name: "noneEmptyMapFieldProp",
+			args: args{
+				prop: &tableaupb.FieldProp{
+					JsonName: "json_name",
+				},
+			},
+			want: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := IsEmptyFieldProp(tt.args.prop); got != tt.want {
+				t.Errorf("IsEmptyFieldProp() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
 func TestExtractMapFieldProp(t *testing.T) {
 	type args struct {
 		prop *tableaupb.FieldProp
diff --git a/test/functest/conf/ScalarFieldProp.json b/test/functest/conf/ScalarFieldProp.json
index 9bae2c2..214d502 100644
--- a/test/functest/conf/ScalarFieldProp.json
+++ b/test/functest/conf/ScalarFieldProp.json
@@ -15,7 +15,8 @@
             "appearance":  {
                 "color":  "red",
                 "shape":  "round"
-            }
+            },
+            "buff_id_1":  100
         }
     }
 }
\ No newline at end of file
diff --git a/test/functest/proto/excel__fieldprop__field_prop.proto b/test/functest/proto/excel__fieldprop__field_prop.proto
index b34c32a..79aa5fa 100644
--- a/test/functest/proto/excel__fieldprop__field_prop.proto
+++ b/test/functest/proto/excel__fieldprop__field_prop.proto
@@ -44,5 +44,6 @@ message ScalarFieldProp {
       string color = 1 [(tableau.field) = {name:"Color"}];
       string shape = 2 [(tableau.field) = {name:"Shape"}];
     }
+    int32 buff_id_1 = 4 [(tableau.field) = {name:"BuffID1"}, json_name="buff_id_1"];
   }
 }
diff --git a/test/functest/testdata/excel/fieldprop/FieldProp#ScalarFieldProp.csv b/test/functest/testdata/excel/fieldprop/FieldProp#ScalarFieldProp.csv
index 589e7f3..2f42904 100644
--- a/test/functest/testdata/excel/fieldprop/FieldProp#ScalarFieldProp.csv
+++ b/test/functest/testdata/excel/fieldprop/FieldProp#ScalarFieldProp.csv
@@ -1,4 +1,4 @@
-ID,Prop1ID,Prop1Flavor,Prop2ID,Prop2Flavor,AppearanceColor,AppearanceShape
-"map<uint32, Item>|{refer:""ItemConf.ID"" range: ""1,100"" sequence: 1 unique:true}","[Prop]int32|{range:""1,10"" fixed:true}",string,int32,string,{Appearance}string,string
-Item1's ID,Prop1's ID,Prop1's flavor,Prop2's ID,Prop2's flavor,Appearance color,Appearance shape
-1,1,sour,2,sweet,red,round
+ID,Prop1ID,Prop1Flavor,Prop2ID,Prop2Flavor,AppearanceColor,AppearanceShape,BuffID1
+"map<uint32, Item>|{refer:""ItemConf.ID"" range: ""1,100"" sequence: 1 unique:true}","[Prop]int32|{range:""1,10"" fixed:true}",string,int32,string,{Appearance}string,string,"int32|{json_name:""buff_id_1""}"
+Item1's ID,Prop1's ID,Prop1's flavor,Prop2's ID,Prop2's flavor,Appearance color,Appearance shape,Buff ID1
+1,1,sour,2,sweet,red,round,100