From ca8ea8d761c02d3ca4da6eaa156acff35d88c510 Mon Sep 17 00:00:00 2001 From: Christian Stewart Date: Fri, 1 Jul 2022 15:13:02 -0700 Subject: [PATCH] feat: add option to use async iterables (#605) Adds option useAsyncIterable which uses AsyncIterable instead of Observable. For example: bidirectionalStreamingRequest( service: string, method: string, data: AsyncIterable ): AsyncIterable Generates Transform async iterables for encoding and decoding: // encodeTransform encodes a source of message objects. // Transform async *encodeTransform( source: AsyncIterable | Iterable ): AsyncIterable { for await (const pkt of source) { if (Array.isArray(pkt)) { for (const p of pkt) { yield* [TestMessage.encode(p).finish()]; } } else { yield* [TestMessage.encode(pkt).finish()]; } } }, // decodeTransform decodes a source of encoded messages. // Transform async *decodeTransform( source: AsyncIterable | Iterable ): AsyncIterable { for await (const pkt of source) { if (Array.isArray(pkt)) { for (const p of pkt) { yield* [TestMessage.decode(p)]; } } else { yield* [TestMessage.decode(pkt)]; } } }, Generates RPC service implementations which use the Transform iterators: BidiStreaming(request: AsyncIterable): AsyncIterable { const data = TestMessage.encodeTransform(request); const result = this.rpc.bidirectionalStreamingRequest('simple.Test', 'BidiStreaming', data); return TestMessage.decodeTransform(result); } AsyncIterables indicate a stream has ended by closing with an optional error. Fixes #600 Signed-off-by: Christian Stewart --- README.markdown | 2 + .../async-iterable-services/parameters.txt | 1 + .../async-iterable-services/simple.bin | Bin 0 -> 384 bytes .../async-iterable-services/simple.proto | 11 ++ integration/async-iterable-services/simple.ts | 138 ++++++++++++++++++ integration/generic-metadata/hero.ts | 2 +- .../example.ts | 2 +- integration/grpc-web/example.ts | 2 +- integration/nestjs-metadata-grpc-js/hero.ts | 2 +- .../nestjs-metadata-observables/hero.ts | 2 +- .../nestjs-metadata-restparameters/hero.ts | 2 +- integration/nestjs-metadata/hero.ts | 2 +- integration/nestjs-simple/hero.ts | 2 +- src/generate-async-iterable.ts | 43 ++++++ src/generate-grpc-web.ts | 29 ++-- src/generate-services.ts | 20 ++- src/main.ts | 5 + src/options.ts | 2 + src/types.ts | 11 +- tests/options-test.ts | 1 + 20 files changed, 248 insertions(+), 31 deletions(-) create mode 100644 integration/async-iterable-services/parameters.txt create mode 100644 integration/async-iterable-services/simple.bin create mode 100644 integration/async-iterable-services/simple.proto create mode 100644 integration/async-iterable-services/simple.ts create mode 100644 src/generate-async-iterable.ts diff --git a/README.markdown b/README.markdown index de04740f2..5b527c292 100644 --- a/README.markdown +++ b/README.markdown @@ -381,6 +381,8 @@ Generated code will be placed in the Gradle build directory. - With `--ts_proto_opt=outputServices=false`, or `=none`, ts-proto will output NO service definitions. +- With `--ts_proto_opt=useAsyncIterable=true`, the generated services will use `AsyncIterable` instead of `Observable`. + - With `--ts_proto_opt=emitImportedFiles=false`, ts-proto will not emit `google/protobuf/*` files unless you explicit add files to `protoc` like this `protoc --plugin=./node_modules/.bin/protoc-gen-ts_proto my_message.proto google/protobuf/duration.proto` diff --git a/integration/async-iterable-services/parameters.txt b/integration/async-iterable-services/parameters.txt new file mode 100644 index 000000000..6d94ef9f6 --- /dev/null +++ b/integration/async-iterable-services/parameters.txt @@ -0,0 +1 @@ +useAsyncIterable=true diff --git a/integration/async-iterable-services/simple.bin b/integration/async-iterable-services/simple.bin new file mode 100644 index 0000000000000000000000000000000000000000..6ad828185091cea054db8e1e6fb2c6769c6717ce GIT binary patch literal 384 zcmZvY!AiqG5Qb;7v&&`@u}rFzq9E~-QxSX8i%=+{;6;6bh)W5ywlvX$59%BE9-U2H zQ1CY2&d>J`OL^O@HcLO*Y}eg-?3^AHBVEGhcS3(wZ1=(PoU&)%b`QR7>xJhbnU8w; z?nmlORq5j%E^i4#h;Qg%*1R@P-PYHu=54{n5CoE7nhhd}E-i*Hw*T>> 3) { + case 1: + message.value = reader.string(); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }, + + // encodeTransform encodes a source of message objects. + // Transform + async *encodeTransform( + source: AsyncIterable | Iterable + ): AsyncIterable { + for await (const pkt of source) { + if (Array.isArray(pkt)) { + for (const p of pkt) { + yield* [TestMessage.encode(p).finish()]; + } + } else { + yield* [TestMessage.encode(pkt).finish()]; + } + } + }, + + // decodeTransform decodes a source of encoded messages. + // Transform + async *decodeTransform( + source: AsyncIterable | Iterable + ): AsyncIterable { + for await (const pkt of source) { + if (Array.isArray(pkt)) { + for (const p of pkt) { + yield* [TestMessage.decode(p)]; + } + } else { + yield* [TestMessage.decode(pkt)]; + } + } + }, + + fromJSON(object: any): TestMessage { + return { + value: isSet(object.value) ? String(object.value) : '', + }; + }, + + toJSON(message: TestMessage): unknown { + const obj: any = {}; + message.value !== undefined && (obj.value = message.value); + return obj; + }, + + fromPartial, I>>(object: I): TestMessage { + const message = createBaseTestMessage(); + message.value = object.value ?? ''; + return message; + }, +}; + +export interface Test { + BidiStreaming(request: AsyncIterable): AsyncIterable; +} + +export class TestClientImpl implements Test { + private readonly rpc: Rpc; + constructor(rpc: Rpc) { + this.rpc = rpc; + this.BidiStreaming = this.BidiStreaming.bind(this); + } + BidiStreaming(request: AsyncIterable): AsyncIterable { + const data = TestMessage.encodeTransform(request); + const result = this.rpc.bidirectionalStreamingRequest('simple.Test', 'BidiStreaming', data); + return TestMessage.decodeTransform(result); + } +} + +interface Rpc { + request(service: string, method: string, data: Uint8Array): Promise; + clientStreamingRequest(service: string, method: string, data: AsyncIterable): Promise; + serverStreamingRequest(service: string, method: string, data: Uint8Array): AsyncIterable; + bidirectionalStreamingRequest( + service: string, + method: string, + data: AsyncIterable + ): AsyncIterable; +} + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin + ? T + : T extends Array + ? Array> + : T extends ReadonlyArray + ? ReadonlyArray> + : T extends {} + ? { [K in keyof T]?: DeepPartial } + : Partial; + +type KeysOfUnion = T extends T ? keyof T : never; +export type Exact = P extends Builtin + ? P + : P & { [K in keyof P]: Exact } & Record>, never>; + +function isSet(value: any): boolean { + return value !== null && value !== undefined; +} diff --git a/integration/generic-metadata/hero.ts b/integration/generic-metadata/hero.ts index c34c95abb..3772395e8 100644 --- a/integration/generic-metadata/hero.ts +++ b/integration/generic-metadata/hero.ts @@ -1,6 +1,6 @@ /* eslint-disable */ -import { Observable } from 'rxjs'; import { Foo } from './some-file'; +import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import * as _m0 from 'protobufjs/minimal'; diff --git a/integration/grpc-web-no-streaming-observable/example.ts b/integration/grpc-web-no-streaming-observable/example.ts index d41f7911c..384b0a7b1 100644 --- a/integration/grpc-web-no-streaming-observable/example.ts +++ b/integration/grpc-web-no-streaming-observable/example.ts @@ -1,8 +1,8 @@ /* eslint-disable */ import { grpc } from '@improbable-eng/grpc-web'; -import { Observable } from 'rxjs'; import { BrowserHeaders } from 'browser-headers'; import { take } from 'rxjs/operators'; +import { Observable } from 'rxjs'; import * as _m0 from 'protobufjs/minimal'; export const protobufPackage = 'rpx'; diff --git a/integration/grpc-web/example.ts b/integration/grpc-web/example.ts index 016f6f1aa..54bf53348 100644 --- a/integration/grpc-web/example.ts +++ b/integration/grpc-web/example.ts @@ -1,8 +1,8 @@ /* eslint-disable */ import { grpc } from '@improbable-eng/grpc-web'; -import { Observable } from 'rxjs'; import { BrowserHeaders } from 'browser-headers'; import { share } from 'rxjs/operators'; +import { Observable } from 'rxjs'; import * as _m0 from 'protobufjs/minimal'; export const protobufPackage = 'rpx'; diff --git a/integration/nestjs-metadata-grpc-js/hero.ts b/integration/nestjs-metadata-grpc-js/hero.ts index fc92d83db..6f1c67d35 100644 --- a/integration/nestjs-metadata-grpc-js/hero.ts +++ b/integration/nestjs-metadata-grpc-js/hero.ts @@ -1,7 +1,7 @@ /* eslint-disable */ import { GrpcMethod, GrpcStreamMethod } from '@nestjs/microservices'; -import { Observable } from 'rxjs'; import { Metadata } from '@grpc/grpc-js'; +import { Observable } from 'rxjs'; export const protobufPackage = 'hero'; diff --git a/integration/nestjs-metadata-observables/hero.ts b/integration/nestjs-metadata-observables/hero.ts index 64dc37d37..f9cfd75f2 100644 --- a/integration/nestjs-metadata-observables/hero.ts +++ b/integration/nestjs-metadata-observables/hero.ts @@ -1,7 +1,7 @@ /* eslint-disable */ import { GrpcMethod, GrpcStreamMethod } from '@nestjs/microservices'; -import { Observable } from 'rxjs'; import { Metadata } from '@grpc/grpc-js'; +import { Observable } from 'rxjs'; export const protobufPackage = 'hero'; diff --git a/integration/nestjs-metadata-restparameters/hero.ts b/integration/nestjs-metadata-restparameters/hero.ts index fe0e557a2..cd80864ee 100644 --- a/integration/nestjs-metadata-restparameters/hero.ts +++ b/integration/nestjs-metadata-restparameters/hero.ts @@ -1,7 +1,7 @@ /* eslint-disable */ import { GrpcMethod, GrpcStreamMethod } from '@nestjs/microservices'; -import { Observable } from 'rxjs'; import { Metadata } from '@grpc/grpc-js'; +import { Observable } from 'rxjs'; export const protobufPackage = 'hero'; diff --git a/integration/nestjs-metadata/hero.ts b/integration/nestjs-metadata/hero.ts index fc92d83db..6f1c67d35 100644 --- a/integration/nestjs-metadata/hero.ts +++ b/integration/nestjs-metadata/hero.ts @@ -1,7 +1,7 @@ /* eslint-disable */ import { GrpcMethod, GrpcStreamMethod } from '@nestjs/microservices'; -import { Observable } from 'rxjs'; import { Metadata } from '@grpc/grpc-js'; +import { Observable } from 'rxjs'; export const protobufPackage = 'hero'; diff --git a/integration/nestjs-simple/hero.ts b/integration/nestjs-simple/hero.ts index 7028e042f..8bcda1c31 100644 --- a/integration/nestjs-simple/hero.ts +++ b/integration/nestjs-simple/hero.ts @@ -1,7 +1,7 @@ /* eslint-disable */ import { GrpcMethod, GrpcStreamMethod } from '@nestjs/microservices'; -import { Observable } from 'rxjs'; import { Timestamp } from './google/protobuf/timestamp'; +import { Observable } from 'rxjs'; import { Empty } from './google/protobuf/empty'; export const protobufPackage = 'hero'; diff --git a/src/generate-async-iterable.ts b/src/generate-async-iterable.ts new file mode 100644 index 000000000..ced7f7c7e --- /dev/null +++ b/src/generate-async-iterable.ts @@ -0,0 +1,43 @@ +import { code, Code } from 'ts-poet'; + +/** Creates a function to transform a message Source to a Uint8Array Source. */ +export function generateEncodeTransform(fullName: string): Code { + return code` + // encodeTransform encodes a source of message objects. + // Transform<${fullName}, Uint8Array> + async *encodeTransform( + source: AsyncIterable<${fullName} | ${fullName}[]> | Iterable<${fullName} | ${fullName}[]> + ): AsyncIterable { + for await (const pkt of source) { + if (Array.isArray(pkt)) { + for (const p of pkt) { + yield* [${fullName}.encode(p).finish()] + } + } else { + yield* [${fullName}.encode(pkt).finish()] + } + } + } + `; +} + +/** Creates a function to transform a Uint8Array Source to a message Source. */ +export function generateDecodeTransform(fullName: string): Code { + return code` + // decodeTransform decodes a source of encoded messages. + // Transform + async *decodeTransform( + source: AsyncIterable | Iterable + ): AsyncIterable<${fullName}> { + for await (const pkt of source) { + if (Array.isArray(pkt)) { + for (const p of pkt) { + yield* [${fullName}.decode(p)] + } + } else { + yield* [${fullName}.decode(pkt)] + } + } + } + `; +} diff --git a/src/generate-grpc-web.ts b/src/generate-grpc-web.ts index 79c7376e9..a4c2639de 100644 --- a/src/generate-grpc-web.ts +++ b/src/generate-grpc-web.ts @@ -1,5 +1,5 @@ import { MethodDescriptorProto, FileDescriptorProto, ServiceDescriptorProto } from 'ts-proto-descriptors'; -import { rawRequestType, requestType, responsePromiseOrObservable, responseType } from './types'; +import { rawRequestType, requestType, responsePromiseOrObservable, responseType, observableType } from './types'; import { Code, code, imp, joinCode } from 'ts-poet'; import { Context } from './context'; import { assertInstanceOf, FormattedMethodDescriptor, maybePrefixPackage } from './utils'; @@ -8,12 +8,11 @@ const grpc = imp('grpc@@improbable-eng/grpc-web'); const share = imp('share@rxjs/operators'); const take = imp('take@rxjs/operators'); const BrowserHeaders = imp('BrowserHeaders@browser-headers'); -const Observable = imp('Observable@rxjs'); /** Generates a client that uses the `@improbable-web/grpc-web` library. */ export function generateGrpcClientImpl( ctx: Context, - fileDesc: FileDescriptorProto, + _fileDesc: FileDescriptorProto, serviceDesc: ServiceDescriptorProto ): Code { const chunks: Code[] = []; @@ -154,18 +153,18 @@ export function addGrpcWebMisc(ctx: Context, hasStreamingMethods: boolean): Code interface UnaryMethodDefinitionishR extends ${grpc}.UnaryMethodDefinition { requestStream: any; responseStream: any; } `); chunks.push(code`type UnaryMethodDefinitionish = UnaryMethodDefinitionishR;`); - chunks.push(generateGrpcWebRpcType(options.returnObservable, hasStreamingMethods)); - chunks.push(generateGrpcWebImpl(options.returnObservable, hasStreamingMethods)); + chunks.push(generateGrpcWebRpcType(ctx, options.returnObservable, hasStreamingMethods)); + chunks.push(generateGrpcWebImpl(ctx, options.returnObservable, hasStreamingMethods)); return joinCode(chunks, { on: '\n\n' }); } /** Makes an `Rpc` interface to decouple from the low-level grpc-web `grpc.invoke and grpc.unary`/etc. methods. */ -function generateGrpcWebRpcType(returnObservable: boolean, hasStreamingMethods: boolean): Code { +function generateGrpcWebRpcType(ctx: Context, returnObservable: boolean, hasStreamingMethods: boolean): Code { const chunks: Code[] = []; chunks.push(code`interface Rpc {`); - const wrapper = returnObservable ? Observable : 'Promise'; + const wrapper = returnObservable ? observableType(ctx) : 'Promise'; chunks.push(code` unary( methodDesc: T, @@ -180,7 +179,7 @@ function generateGrpcWebRpcType(returnObservable: boolean, hasStreamingMethods: methodDesc: T, request: any, metadata: grpc.Metadata | undefined, - ): ${Observable}; + ): ${observableType(ctx)}; `); } @@ -189,7 +188,7 @@ function generateGrpcWebRpcType(returnObservable: boolean, hasStreamingMethods: } /** Implements the `Rpc` interface by making calls using the `grpc.unary` method. */ -function generateGrpcWebImpl(returnObservable: boolean, hasStreamingMethods: boolean): Code { +function generateGrpcWebImpl(ctx: Context, returnObservable: boolean, hasStreamingMethods: boolean): Code { const options = code` { transport?: grpc.TransportFactory, @@ -212,13 +211,13 @@ function generateGrpcWebImpl(returnObservable: boolean, hasStreamingMethods: boo `); if (returnObservable) { - chunks.push(createObservableUnaryMethod()); + chunks.push(createObservableUnaryMethod(ctx)); } else { chunks.push(createPromiseUnaryMethod()); } if (hasStreamingMethods) { - chunks.push(createInvokeMethod()); + chunks.push(createInvokeMethod(ctx)); } chunks.push(code`}`); @@ -260,13 +259,13 @@ function createPromiseUnaryMethod(): Code { `; } -function createObservableUnaryMethod(): Code { +function createObservableUnaryMethod(ctx: Context): Code { return code` unary( methodDesc: T, _request: any, metadata: grpc.Metadata | undefined - ): ${Observable} { + ): ${observableType(ctx)} { const request = { ..._request, ...methodDesc.requestType }; const maybeCombinedMetadata = metadata && this.options.metadata @@ -293,13 +292,13 @@ function createObservableUnaryMethod(): Code { `; } -function createInvokeMethod() { +function createInvokeMethod(ctx: Context) { return code` invoke( methodDesc: T, _request: any, metadata: grpc.Metadata | undefined - ): ${Observable} { + ): ${observableType(ctx)} { // Status Response Codes (https://developers.google.com/maps-booking/reference/grpc-api/status_codes) const upStreamCodes = [2, 4, 8, 9, 10, 13, 14, 15]; const DEFAULT_TIMEOUT_TIME: number = 3_000; diff --git a/src/generate-services.ts b/src/generate-services.ts index f64768591..8f258bdaa 100644 --- a/src/generate-services.ts +++ b/src/generate-services.ts @@ -7,10 +7,10 @@ import { rawRequestType, responsePromiseOrObservable, responseType, + observableType, } from './types'; import { assertInstanceOf, FormattedMethodDescriptor, maybeAddComment, maybePrefixPackage, singular } from './utils'; import SourceInfo, { Fields } from './sourceInfo'; -import { camelCase } from './case'; import { contextTypeVar } from './main'; import { Context } from './context'; @@ -34,7 +34,7 @@ export function generateService( sourceInfo: SourceInfo, serviceDesc: ServiceDescriptorProto ): Code { - const { options, utils } = ctx; + const { options } = ctx; const chunks: Code[] = []; maybeAddComment(sourceInfo, chunks, serviceDesc.options?.deprecated); @@ -121,12 +121,20 @@ function generateRegularRpcMethod( decode = code`data => ${utils.fromTimestamp}(${rawOutputType}.decode(new ${Reader}(data)))`; } if (methodDesc.clientStreaming) { - encode = code`request.pipe(${imp('map@rxjs/operators')}(request => ${encode}))`; + if (options.useAsyncIterable) { + encode = code`${rawInputType}.encodeTransform(request)`; + } else { + encode = code`request.pipe(${imp('map@rxjs/operators')}(request => ${encode}))`; + } } let returnVariable: string; if (options.returnObservable || methodDesc.serverStreaming) { returnVariable = 'result'; - decode = code`result.pipe(${imp('map@rxjs/operators')}(${decode}))`; + if (options.useAsyncIterable) { + decode = code`${rawOutputType}.decodeTransform(result)`; + } else { + decode = code`result.pipe(${imp('map@rxjs/operators')}(${decode}))`; + } } else { returnVariable = 'promise'; decode = code`promise.then(${decode})`; @@ -207,7 +215,7 @@ export function generateServiceClientImpl( } /** We've found a BatchXxx method, create a synthetic GetXxx method that calls it. */ -function generateBatchingRpcMethod(ctx: Context, batchMethod: BatchMethod): Code { +function generateBatchingRpcMethod(_ctx: Context, batchMethod: BatchMethod): Code { const { methodDesc, singleMethodName, @@ -315,7 +323,7 @@ export function generateRpcType(ctx: Context, hasStreamingMethods: boolean): Cod const maybeContextParam = options.context ? 'ctx: Context,' : ''; const methods = [[code`request`, code`Uint8Array`, code`Promise`]]; if (hasStreamingMethods) { - const observable = imp('Observable@rxjs'); + const observable = observableType(ctx); methods.push([code`clientStreamingRequest`, code`${observable}`, code`Promise`]); methods.push([code`serverStreamingRequest`, code`Uint8Array`, code`${observable}`]); methods.push([ diff --git a/src/main.ts b/src/main.ts index 2ba1ed230..5b0fdc68d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -67,6 +67,7 @@ import { generateGrpcMethodDesc, generateGrpcServiceDesc, } from './generate-grpc-web'; +import { generateEncodeTransform, generateDecodeTransform } from './generate-async-iterable'; import { generateEnum } from './enums'; import { visit, visitServices } from './visit'; import { DateOption, EnvOption, LongOption, OneofOption, Options, ServiceOption } from './options'; @@ -165,6 +166,10 @@ export function generateFile(ctx: Context, fileDesc: FileDescriptorProto): [stri staticMembers.push(generateEncode(ctx, fullName, message)); staticMembers.push(generateDecode(ctx, fullName, message)); } + if (options.useAsyncIterable) { + staticMembers.push(generateEncodeTransform(fullName)); + staticMembers.push(generateDecodeTransform(fullName)); + } if (options.outputJsonMethods) { staticMembers.push(generateFromJson(ctx, fullName, fullTypeName, message)); staticMembers.push(generateToJson(ctx, fullName, fullTypeName, message)); diff --git a/src/options.ts b/src/options.ts index 22f2dfde8..a449ad87e 100644 --- a/src/options.ts +++ b/src/options.ts @@ -61,6 +61,7 @@ export type Options = { onlyTypes: boolean; emitImportedFiles: boolean; useExactTypes: boolean; + useAsyncIterable: boolean; unknownFields: boolean; usePrototypeForDefaults: boolean; useJsonWireFormat: boolean; @@ -99,6 +100,7 @@ export function defaultOptions(): Options { onlyTypes: false, emitImportedFiles: true, useExactTypes: true, + useAsyncIterable: false, unknownFields: false, usePrototypeForDefaults: false, useJsonWireFormat: false, diff --git a/src/types.ts b/src/types.ts index 8243bb0f3..77e9ceef5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -660,6 +660,13 @@ export function rawRequestType(ctx: Context, methodDesc: MethodDescriptorProto): return messageToTypeName(ctx, methodDesc.inputType); } +export function observableType(ctx: Context): Code { + if (ctx.options.useAsyncIterable) { + return code`AsyncIterable`; + } + return code`${imp('Observable@rxjs')}`; +} + export function requestType(ctx: Context, methodDesc: MethodDescriptorProto, partial: boolean = false): Code { let typeName = rawRequestType(ctx, methodDesc); @@ -668,7 +675,7 @@ export function requestType(ctx: Context, methodDesc: MethodDescriptorProto, par } if (methodDesc.clientStreaming) { - return code`${imp('Observable@rxjs')}<${typeName}>`; + return code`${observableType(ctx)}<${typeName}>`; } return typeName; } @@ -686,7 +693,7 @@ export function responsePromise(ctx: Context, methodDesc: MethodDescriptorProto) } export function responseObservable(ctx: Context, methodDesc: MethodDescriptorProto): Code { - return code`${imp('Observable@rxjs')}<${responseType(ctx, methodDesc)}>`; + return code`${observableType(ctx)}<${responseType(ctx, methodDesc)}>`; } export function responsePromiseOrObservable(ctx: Context, methodDesc: MethodDescriptorProto): Code { diff --git a/tests/options-test.ts b/tests/options-test.ts index 8c45aec96..bd772f641 100644 --- a/tests/options-test.ts +++ b/tests/options-test.ts @@ -37,6 +37,7 @@ describe('options', () => { "stringEnums": false, "unknownFields": false, "unrecognizedEnum": true, + "useAsyncIterable": false, "useDate": "timestamp", "useExactTypes": true, "useJsonWireFormat": false,