七、以类型安全的方式使用 JSON

在本章中,我们基于第 1 章在客户端上读写 JSON,展示了如何使用 c#、Java 和 TypeScript 在应用中使用 JSON 强类型。 你会发现以下食谱:

  • 如何使用 Json 反序列化对象。 净
  • 如何使用 Json 处理日期和时间对象。 净
  • 如何使用 Java 的 gson 来反序列化一个对象
  • 如何在 Node.js 中使用 TypeScript
  • 如何使用 TypeScript 注释简单的类型
  • 如何使用 TypeScript 声明接口
  • 如何使用 TypeScript 声明带有接口的类
  • 使用 json2ts 从 JSON 生成 TypeScript 接口

简介

虽然有些人说强类型是为弱智准备的,但事实是,编程语言中的强类型可以帮助您避免错误的整个类,在这些类中,您错误地假设一个类型的对象实际上是另一个类型。 c#和 Java 等语言提供强类型正是出于这个原因。

幸运的是,c#和 Java 的 JSON 序列化器支持强类型,当您确定了对象表示并只是想将 JSON 映射到已经定义的类的实例时,这种方法特别方便。 在第 1 章在客户端上读写 JSON,您看到了如何从 c#或 Java 类转换为 JSON,以及如何将 JSON 转换回非类型化对象; 在本章中,我们使用 Json。 NET 用于 c#, gson 用于 Java,可以将 JSON 转换为应用中定义的类的实例。

最后,我们来看看 TypeScript,它是 JavaScript 的一个扩展,提供编译时类型检查,编译成普通的 JavaScript,供 Node.js 和浏览器使用。 我们将学习如何为 Node.js 安装 TypeScript 编译器,如何使用 TypeScript 注释类型和接口,以及如何使用 Timmy Kokke 的网页从 JSON 对象自动生成 TypeScript 接口。

如何使用 Json 反序列化一个对象。 净

在这个食谱中,我们向您展示如何使用 Newtonsoft 的 Json。 NET 将 JSON 反序列化为一个类的实例对象。 我们将使用 Json。 ,在客户端上读和写 JSON,因为尽管它与现有的。NET JSON 序列化器一起工作,我想让你知道关于 JSON 的其他事情。 NET,我们将在接下来的两个菜谱中讨论它。

准备好

首先,您需要确保有一个对 Json 的引用。 。NET。 最简单的方法是使用 NuGet; 启动 NuGet,搜索 Json。 然后点击安装,如下图所示:

Getting ready

你还将需要引用Newonsoft.Json命名空间在任何文件中,需要这些类的using指令在你的文件顶部:

usingNewtonsoft.Json;

How to do it…

下面的例子提供了一个简单类的实现,将一个 JSON 字符串转换为该类的一个实例,然后将实例转换回 JSON:

using System;
usingNewtonsoft.Json;

namespaceJSONExample
{

  public class Record
  {
    public string call;
    public double lat;
    public double lng;
  }

  class Program
  {
    static void Main(string[] args)
      {
        String json = @"{ 'call': 'kf6gpe-9', 
        'lat': 21.9749, 'lng': 159.3686 }";

        var result = JsonConvert.DeserializeObject<Record>(
          json, newJsonSerializerSettings
            {
        MissingMemberHandling = MissingMemberHandling.Error
          });
        Console.Write(JsonConvert.SerializeObject(result));

        return;
        }
  }
}

How it works…

在中,为了以一种类型安全的方式对 JSON 进行反序列化,我们需要一个具有与 JSON 相同字段的类。 在前几行中定义的Record类就是这样做的,它定义了calllatlng的字段。

Newtonsoft.Json命名空间为JsonConvert类提供了SerializeObjectDeserializeObject静态方法。 DeserializeObject是一个通用方法,接受应该返回的对象类型作为类型参数,作为要解析的 JSON 参数,以及一个指示 JSON 解析选项的可选参数。 我们将MissingMemberHandling属性作为设置传递,用枚举Error的值指示在字段丢失的情况下,解析器应该抛出异常。 解析类之后,我们将其再次转换为 JSON,并将生成的 JSON 写入控制台。

There's more…

如果您跳过了MissingMember选项或传递Ignore(默认),那么您的 JSON 和类中的字段名可能会不匹配,这可能不是您想要的类型安全转换。 您还可以传递值为IncludeIgnoreNullValueHandling字段。 如果Include,包含空值字段; 如果Ignore,Null 值的字段将被忽略。

参见

Json 的完整文档 网址:http://www.newtonsoft.com/json/help/html/Introduction.htm

使用.NET 序列化器支持 JSON 也可以实现类型安全的反序列化; 语法是相似的。 例如,请参阅 JavaScriptSerializer 类的文档https://msdn.microsoft.com/en-us/library/system.web.script.serialization.javascriptserializer(v=vs.110).aspx

如何使用 Json 处理日期和时间对象 净

JSON 中的日期对人们来说是有问题的,因为 JavaScript 的日期是以毫秒为单位的,这通常是不可读的。 不同的 JSON 解析器处理方法不同; Json。 NET 有一个很好的IsoDateTimeConverter,它将日期和时间格式化为 ISO 格式,使其易于在除 JavaScript 以外的平台上调试或解析。 通过创建新的转换器对象,并使用转换器对象将一种值类型转换为另一种值类型,您也可以扩展此方法,将任何类型的格式化数据转换为 JSON 属性。

How to do it…

简单地包括一个新的IsoDateTimeConverter对象当你调用JsonConvert.Serialize,像这样:

string json = JsonConvert.SerializeObject(p, 
newIsoDateTimeConverter());

How it works…

这会导致序列化器调用任何日期和时间对象实例的IsoDateTimeConverter实例,在 JSON 中返回这样的 ISO 字符串:

2015-07-29T08:00:00

There's more…

注意,这可以通过 Json 进行解析。 NET,而不是 JavaScript; 在 JavaScript 中,你会想要使用这样的函数:

Function isoDateReviver(value) {
  if (typeof value === 'string') {
  var a = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)(?:([\+-])(\d{2})\:(\d{2}))?Z?$/
  .exec(value);
  if (a) {
     var utcMilliseconds = Date.UTC(+a[1], 
          +a[2] - 1, 
          +a[3], 
          +a[4], 
          +a[5], 
          +a[6]);
        return new Date(utcMilliseconds);
    }
  }
return value;
}

第三行上的非常复杂的正则表达式匹配 ISO 格式的日期,提取每个字段。 如果正则表达式找到匹配,它提取每个日期字段,然后Date类的 UTC 方法使用这些字段来创建一个新的日期。

提示

请注意,整个正则表达式(在/字符之间的所有字符)应该在一行中,不能有空格。 然而,对于这个页面来说,它有点长!

参见

关于 Json 的更多信息。 NET 句柄的日期和时间,见文档和示例在http://www.newtonsoft.com/json/help/html/SerializeDateFormatHandling.htm

如何使用 Java 的 gson 来反序列化对象

比如 Json。 NET, gson 提供了一种方法来指定要反序列化 JSON 对象的目标类。 事实上,这与你在在 Java中读写 JSON、第 1 章客户端中使用的方法相同。

准备好

您需要在应用中包含 gson JAR 文件,就像您将其他任何外部 API 一样。

How to do it…

使用的方法与使用fromJson的 gson 解析类型不安全 JSON 时使用的方法相同,只是将类对象作为第二个参数传递给 gson,如下所示:

// Assuming we have a class Record that looks like this:
/*
class Record {
  private String call;
  private float lat;
  private float lng;
    // public API would access these fields
}
*/

Gson gson = new com.google.gson.Gson(); 
String json = "{ \"call\": \"kf6gpe-9\", 
\"lat\": 21.9749, \"lng\": 159.3686 }";
Record result = gson.fromJson(json, Record.class);

How it works…

fromGson方法总是接受一个 Java 类; 在第 1 章客户端上读写 JSON中,我们反序列化的类是JsonElement,它处理 JSON 的一般动态特性。 在本例中,我们直接转换为应用可以使用的普通旧 Java 对象,而不需要使用 gson 提供的JsonElement的解引用和类型转换接口。

There's more…

gson 库还可以处理嵌套类型和数组。 您还可以通过声明字段transient来隐藏字段,使其不被序列化或反序列化,这是有意义的,因为瞬态字段不是序列化的。

参见

gson 的文档及其对类的反序列化实例的支持在https://sites.google.com/site/gson/gson-user-guide#TOC-Object-Examples

如何在 Node.js 中使用 TypeScript

在 Visual Studio 中使用 TypeScript 很容易; 对于 Visual Studio 2013 Update 2 之后的任何版本,它只是 Visual Studio 安装的的一部分。 为 Node.js 获取 TypeScript 编译器几乎同样容易——这是一个npm install之事。

How to do it…

在路径中有 npm 的命令行中,运行以下命令:

npm install –g typescript

npm 选项–g告诉 npm 全局安装 TypeScript 编译器,这样你编写的每个 Node.js 应用都可以使用它。 一旦你运行了它,npm 就会为你的平台下载并安装 TypeScript 编译器二进制文件。

There's more…

一旦你运行这个命令来安装编译器,你就可以在命令行中使用 TypeScript 编译器 tsc 了。 用 tsc 编译文件就像编写源代码并保存在以.ts扩展名结尾的文件中一样简单,然后在上面运行 tsc。 例如,给定以下 TypeScript 文件hello.ts:

function greeter(person: string) {
  return "Hello, " + person;
}

var user: string = "Ray";

console.log(greeter(user));

在命令行运行tschello.ts创建以下 JavaScript:

function greeter(person) {
  return "Hello, " + person;
}

var user = "Ray";

console.log(greeter(user));

试一试!

我们将在下一节看到,greeter的函数声明包含一个 TypeScript 注释; 它声明参数人为string。 在hello.ts的底部添加以下一行:

console.log(greeter(2));

现在,再次运行tschello.ts命令; 你会得到这样一个错误:

C:\Users\rarischp\Documents\node.js\typescript\hello.ts(8,13): error TS2082: Supplied parameters do not match any signature of call target:
        Could not apply type 'string' to argument 1 which is of type 'number'.
C:\Users\rarischp\Documents\node.js\typescript\hello.ts(8,13): error TS2087: Could not select overload for 'call' expression.

这个错误表明我试图用一个错误类型的值调用greeter,传递一个greeter期望字符串的数字。 在下一篇文章中,我们将介绍 TypeScript 对简单类型的类型注释支持。

参见

TypeScript 首页有教程和参考文档,位于http://www.typescriptlang.org/

如何使用 TypeScript 注释简单的类型

TypeScript 中的类型注释是附加在冒号后面的变量或函数的简单装饰器。 它支持与 JavaScript 中相同的基本类型,并支持声明接口和类,这一点我们将在后面讨论。

How to do it…

下面是一些简单的变量声明和两个函数声明的例子:

function greeter(person: string): string {
  return "Hello, " + person;
}

function circumference(radius: number) : number {
  var pi: number = 3.141592654;
  return 2 * pi * radius;
}

var user: string = "Ray";

console.log(greeter(user));
console.log("You need " + 
circumference(2) + 
  " meters of fence for your dog.");

这个示例展示了如何注释函数和变量。

How it works…

变量——无论是独立的变量还是作为函数参数的变量——都使用冒号和类型进行修饰。 例如,第一个函数greeter接受一个参数 person,它必须是一个字符串。 第二个函数,circumference,接受一个半径,它必须是一个数字,并在它的作用域中声明了一个单独的变量pi,它必须是一个数字,其值为3.141592654

您可以像在 JavaScript 中那样以正常的方式声明函数,然后在函数名之后添加类型注释,同样使用冒号和类型。 因此,greeter返回一个字符串,circumference返回一个数字。

There's more…

TypeScript 定义了以下基本类型装饰器,它们映射到它们的底层 JavaScript 类型:

  • array:此为复合类型。 例如,你可以这样写一个字符串列表:
  • boolean:这个类型装饰器可以包含truefalse值。
  • number:这个类型装饰器就像 JavaScript 本身,可以是任何浮点数。
  • string:这个类型装饰器是一个字符串。
  • enum:一个枚举,用enum关键字写,如下:

    enumColor { Red = 1, Green, Blue }; var c : Color = Color.Blue;

  • any:该类型表示该变量可以是任何类型。

  • void:该类型表示该值没有类型。 您将使用void来指示一个不返回任何内容的函数。

参见

关于 TypeScript 类型的列表,请参见 TypeScript 手册http://www.typescriptlang.org/Handbook

如何使用 TypeScript 声明接口

一个接口定义如何行为,而不定义实现。 在 TypeScript 中,接口通过描述它的字段来命名复杂类型。 这就是所谓的结构子类型。

How to do it…

声明接口有点像声明结构或类; 在接口中定义字段,每个字段都有自己的类型,如下所示:

interface Record {
  call: string;
  lat: number;
  lng: number;
}

Function printLocation(r: Record) {
  console.log(r.call + ': ' + r.lat + ', ' + r.lng);
}

var myObj = {call: 'kf6gpe-7', lat: 21.9749, lng: 159.3686};

printLocation(myObj);

How it works…

TypeScript 中的interface关键字定义了一个接口; 正如我已经提到的,接口由它声明的字段及其类型组成。 在这个清单中,我定义了一个普通的 JavaScript 对象myObj,然后调用之前定义的函数printLocation,该函数的参数为Record。 当使用myObj调用printLocation时,TypeScript 编译器会检查字段并输入每个字段,只有当对象与接口匹配时才允许调用printLocation

There's more…

小心! TypeScript 只能提供编译类型检查。 你认为下面的代码是做什么的?

interface Record {
  call: string;
  lat: number;
  lng: number;
}

Function printLocation(r: Record) {
  console.log(r.call + ': ' + r.lat + ', ' + r.lng);
}

var myObj = {call: 'kf6gpe-7', lat: 21.9749, lng: 159.3686};
printLocation(myObj);

var json = '{"call":"kf6gpe-7","lat":21.9749}';
var myOtherObj = JSON.parse(json);
printLocation(myOtherObj);

首先,使用tsc可以很好地编译。 当你用 node 运行它时,你会看到以下内容:

kf6gpe-7: 21.9749, 159.3686
kf6gpe-7: 21.9749, undefined

发生了什么事? TypeScript 编译器不会在你的代码中添加运行时类型检查,所以你不能把一个接口强加给一个运行时创建的非字面量对象。 在这个例子中,由于 JSON 中缺少lng字段,函数无法打印它,而是打印值undefined

然而,这并不意味着你不应该在 JSON 中使用 TypeScript。 类型注释为代码的所有读者服务,无论是编译器还是人。 您可以使用类型注释来表明您作为开发人员的意图,代码的读者可以更好地理解您所编写的代码的设计和限制。

参见

有关接口的更多信息,请参阅 TypeScript 文档http://www.typescriptlang.org/Handbook#interfaces

如何使用 TypeScript 声明带有接口的类

接口允许您指定行为而不指定实现; 类允许您将实现细节封装在接口后面。 TypeScript 类可以封装字段或方法,就像其他语言中的类一样。

How to do it…

下面是我们的 Record 结构的一个例子,这次是一个带有接口的类:

class RecordInterface {
  call: string;
  lat: number;
  lng: number;

  constructor(c: string, la: number, lo: number) {}
  printLocation() {}

}

class Record implements RecordInterface {
  call: string;
  lat: number;
  lng: number;

  constructor(c: string, la: number, lo: number) {
    this.call = c;
    this.lat = la;
    this.lng = lo;
  }

  printLocation() {
    console.log(this.call + ': ' + this.lat + ', ' + this.lng);
  }
}

var myObj : Record = new Record('kf6gpe-7', 21.9749, 159.3686);

myObj.printLocation();

How it works…

关键字interface同样定义了一个接口,如上一节所示。 你以前没见过的class关键字实现了一个类; 可选关键字implements表示该类实现接口RecordInterface

注意:实现接口的类必须具有接口规定的所有相同的字段和方法; 否则,它就不符合接口的要求。 因此,我们的Record类包含了calllatlng的字段,这些字段的类型与接口中的相同,还有方法构造函数和printLocation

构造函数方法是一个特殊的方法,当你使用new创建一个类的新实例时调用。 注意,对于类,与普通对象不同,创建类的正确方法是使用构造函数,而不是仅仅将它们构建为字段和值的集合。 我们在清单的第二行到最后一行执行此操作,将构造函数参数作为函数参数传递给类构造函数。

参见

使用类可以做的事情还有很多,包括定义继承和创建公共和私有字段和方法。 有关 TypeScript 中类的更多信息,请参见http://www.typescriptlang.org/Handbook#classes文档中的。

使用 json2ts 从 JSON 生成 TypeScript 接口

这个最后的食谱更像是一个提示而不是食谱; 如果你使用另一种编程语言或手工开发了一些 JSON,你可以通过使用 Timmy Kokke 的 json2ts 网站,轻松地为对象创建一个包含 JSON 的 TypeScript 接口。

How to do it…

只需点击http://json2ts.com,将 JSON 粘贴到出现的框中,然后点击生成 TypeScript 按钮。 你会得到第二个文本框的奖励,它会显示 TypeScript 接口的定义,你可以将它保存为自己的文件,并包含在你的 TypeScript 应用中。

How it works…

下图是一个简单的示例:

How it works…

你可以把这个 typescript 文件保存为自己的文件,也就是一个definition文件,后缀是.d.ts,然后在你的 typescript 中使用import关键字包含该模块,如下所示:

import module = require('module');