前言

在与后端对接过程中,常常会出现因为后端不规范,导致某些String字段有时传null,有时传""。但我们在对接时并不知道哪些可能为空,他到底会传啥,总不能将Bean类中的所有字段都标为?,不合适,但是不标为?的话,GsonConverterFactory会将其转为"null"字符串,导致页面的一些控件显示为null很不合适,所以需要我们统一对String字段却传来的null值进行处理转为""空字符串。

同理,对于是List集合的字段,后端有时不传[],而是传个null,我们也需要对这种情况进行统一处理,将其转为[]

这里我们使用TypeAdapter对其进行处理,我们先了解一下TypeAdapter这个类

1. 内容解析

1.1 TypeAdapter

1.1.1 解释

该类中有两个方法write()read()

write()

public abstract void write(JsonWriter out, T value) throws IOException;
  /**
* Writes one JSON value (an array, object, string, number, boolean or null)
* for {@code value}.
*
* @param value the Java object to write. May be null.
*/

为value写入一个JSON数据

read()

public abstract T read(JsonReader in) throws IOException;
/**
* Reads one JSON value (an array, object, string, number, boolean or null)
* and converts it to a Java object. Returns the converted object.
*
* @return the converted Java object. May be null.
*/

读取一个JSON数据并将其转换为Object对象

1.1.2 示例

看上面的解释可能不好理解,不如我们试试官方给的示例

class PointTypeAdapter : TypeAdapter<Point>() {
override fun write(out: JsonWriter, value: Point) {
if (value == null) {
out.nullValue()
return
}
var xy = "${value.x},${value.y}"
out.value(xy)
}
override fun read(reader: JsonReader): Point? {
if (reader.peek() == JsonToken.NULL) {
reader.nextNull()
return null
}
var xy = reader.nextString()
var parts = xy.split(",")
var x = Integer.parseInt(parts[0])
var y = Integer.parseInt(parts[1])
return Point(x, y)
}
}

1.2 TypeAdapterFactory

创建完TypeAdapter,那该如何使用呢,我们怎么控制什么时候使用该PointTypeAdapter解析,什么时候不用它呢?这里用到了TypeAdapterFactory

class MyTypeAdapterFactory : TypeAdapterFactory {

    override fun <T : Any?> create(gson: Gson?, type: TypeToken<T>): TypeAdapter<T>? {
val rawType = type.rawType
if (rawType== Point::class.java){
return PointTypeAdapter() as TypeAdapter<T>
}
return null
}
}

通过getRawType()获取此类型的原始类型,当类型为Point类型时,返回PointTypeAdapter(),代表使用它来进行解析,如果不是返回null,则是默认的解析方式。

注意:

其实我们不需要TypeAdapterFactory也可以使用TypeAdapter,如下所示,针对基本类型使用registerTypeAdapter()方法,而针对其他类型,要使用registerTypeHierarchyAdapter()方法,当然也可以这两个连用

val gson = GsonBuilder()
.registerTypeAdapter(String::class.java,StringTypeAdapter())
.registerTypeHierarchyAdapter(Point::class.java,PointTypeAdapter())
.create()

1.3 Gson对象创建

既然要解析数据,我们就要创建Gson()对象,平时我们是通过val gson = Gson()直接创建,要使用TypeAdapterFactory,我们通过以下方式创建

val typeAdapterGson =GsonBuilder().registerTypeAdapterFactory(MyTypeAdapterFactory()).create()

1.4 测试使用

通过以下例子,感受下

data class Graph(var origin:String,var points:List<Point>)
//解析该Json字符串:{"origin":"0.0","points":["1,2"]}
//通过PointTypeAdapter处理的
val gson = GsonBuilder().registerTypeAdapterFactory(MyTypeAdapterFactory()).create()
val graphAdapter = gson.getAdapter(Graph::class.java) //普通的处理方式
val gson1 = Gson()
val graphAdapter1 = gson1.getAdapter(Graph::class.java) val json = "{\"origin\":\"0.0\",\"points\":[\"1,2\"]}"
//PointTypeAdapter处理
val graph = graphAdapter.fromJson(json)
Log.i(TAG, "graph: $graph")
try {
//未经PointTypeAdapter处理正常解析的
val graph1 = graphAdapter1.fromJson(json)
Log.i(TAG, "graph1: $graph1")
} catch (e: Exception) {
Log.i(TAG, "解析生成graph1失败:${e.message}")
}

打印的结果如下:

graph: Graph(origin=0.0, points=[Point(1, 2)])

解析生成graph1失败:java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 28 path $.points[0]

可以看到,将String转为Object对象时,使用到了PointTypeAdapter的read()方法

对于{"origin":"0.0","points":["1,2"]}这样的数据,如果我们不做任何处理,直接转Graph,是会转化失败的,因为"points":["1,2"]并不能转为List<Point>,但经过PointTypeAdapterread()方法处理后,"points":["1,2"]这样的数据我们可以转化为List<Point>

val point = Point(1, 2)
val graph2 = Graph("0.0", listOf(point))
val json1 = graphAdapter.toJson(graph2)
Log.i(TAG, "PointTypeAdapter处理生成的: $json1")
val json2 = graphAdapter1.toJson(graph2)
Log.i(TAG, "未经PointTypeAdapter处理生成的: $json2")

打印的结果如下:

PointTypeAdapter处理生成的: {"origin":"0.0","points":["1,2"]}
未经PointTypeAdapter处理生成的: {"origin":"0.0","points":[{"x":1,"y":2}]}

可以看到,将Object转为JSON数据时,用到了PointTypeAdapter的write()方法

1.5 小结

所以使用TypeAdapter

  1. 将String转为Object时会用到read()方法

  2. 将Object转为String时会用到write()方法

所以,我们想统一对字符串传来的null值进行处理,相当于在String转为Object时,只需要我们创建TypeAdapter,并在read()方法里进行处理即可。

2. null转为""具体实现

2.1 创建TypeAdapter

class StringAdapter : TypeAdapter<String>() {
//此方法是将Object转为Json数据
override fun write(out: JsonWriter, value: String?) {
if(value==null){
out.nullValue()
return
}
out.value(value)
} //读取时,会将null转为""
override fun read(jsonReader: JsonReader): String {
//peek()返回下一个令牌的类型,但并不使用它
if (jsonReader.peek() == JsonToken.NULL) {
//使用流中的下一个令牌
//nextNull()如果下一个字段不是null或者流关闭,就会报错
jsonReader.nextNull()
return ""
}
return jsonReader.nextString()
}
}

将数据解析为Object对象时,我们会用到read()方法,当jsonReader.peek() == JsonToken.NULL下一个令牌是NULL时,即后端给我们的String字段传的是null时,我们返回""空字符串,否则就正常返回。因为我们的TypeAdapter是针对String字段,所以我们使用它jsonReader.nextString()正常返回即可。

注意: 因为是流处理,我们虽然返回了"",但是我们需要处理掉流中的null令牌,调用jsonReader.nextNull()方法,否则会报错Expected a name but was NULL at line 3 column 20 path $.testParam

如果我们没有处理该null令牌,导致下次解析不是NULL类型值时,但使用的是null,导致解析错误。

2.2 创建TypeAdapterFactory

class MyTypeAdapterFactory : TypeAdapterFactory {

    override fun <T : Any?> create(gson: Gson?, type: TypeToken<T>): TypeAdapter<T>? {
val rawType = type.rawType if (rawType == String::class.java) {
return StringAdapter() as TypeAdapter<T>
}
return null
}
}

2.3 创建Gson对象

val gson = GsonBuilder().registerTypeAdapterFactory(MyTypeAdapterFactory()).create()

2.4 将GsonConverterFactory的create()方法内添加该Gson对象

val retrofit =   Retrofit.Builder().addConverterFactory(GsonConverterFactory.create(gson))
.baseUrl("http://192.168.14.57:4523/mock/1059885/")
.build()

2.5 测试使用

2.5.1 网络请求

使用Apifox来创建网络接口,后续再单独写一篇介绍如何使用该软件,是真的好用

2.5.1.1 创建接口

//获取String字段为null的数据
@GET("nullString")
fun getNullStringData(): Call<TestBean>

该接口返回的数据如下

{
"code": 200,
"testParam": null
}

2.5.1.2 未使用TypeAdapter进行解析

val normalRetrofit =
Retrofit.Builder().addConverterFactory(GsonConverterFactory.create())
.baseUrl("http://192.168.14.57:4523/mock/1059885/")
.build() val normalApiService = normalRetrofit.create(ApiService::class.java) normalApiService.getNullStringData().enqueue(object : Callback<TestBean> {
override fun onResponse(call: Call<TestBean>, response: Response<TestBean>) {
response.body()?.let {
tvMsg.text = "code:${it.code}\n testParam:${it.testParam}"
Log.i(TAG, "onResponse: $it")
}
} override fun onFailure(call: Call<TestBean>, t: Throwable) {
Log.i(TAG, "onFailure: ${t.message}")
tvMsg.text = "请求失败:${t.message}"
}
})

打印出来的数据如下

TestBean(testParam=null, code=200)

2.5.1.3 使用了TypeAdapter处理的解析

val typeAdapterRetrofit =
Retrofit.Builder().addConverterFactory(GsonConverterFactory.create(typeAdapterGson))
.baseUrl("http://192.168.14.57:4523/mock/1059885/")
.build() val typeAdapterApiService = typeAdapterRetrofit.create(ApiService::class.java) typeAdapterApiService.getNullStringData().enqueue(object : Callback<TestBean> {
override fun onResponse(call: Call<TestBean>, response: Response<TestBean>) {
response.body()?.let {
tvMsg.text = "code:${it.code}\n testParam:${it.testParam}"
Log.i(TAG, "onResponse: $it")
}
} override fun onFailure(call: Call<TestBean>, t: Throwable) {
Log.i(TAG, "onFailure: ${t.message}")
tvMsg.text = "请求失败:${t.message}"
}
})

打印出来的数据如下

TestBean(testParam=, code=200)

2.5.1.4 小结

可以看到,使用了StringTypeAdapter,在面对字段类型为String,但传来的值为null的情况时,会将其转为""

2.5.2 本地测试

如果你不想创建接口,也可以来通过解析{"code":200,"testParam":null}本地验证下

val str = "{\n" +
" \"code\": 200,\n" +
" \"testParam\": null\n" +
"}"
val testBean1 = normalGson.fromJson<TestBean>(str, TestBean::class.java)
val testBean2 = typeAdapterGson.fromJson<TestBean>(str, TestBean::class.java)
//看会将null转为什么
Log.i(TAG, "onCreate: str1:$testBean1")
Log.i(TAG, "onCreate: str2:$testBean2")

打印出来的数据如下

str1:TestBean(testParam=null, code=200)
str2:TestBean(testParam=, code=200)

可以看的,使用TypeAdapter的Gson对象,会将类型为String但值为null的数据直接转为""

3 null转为[]具体实现

3.1 创建TypeAdapter

class CollectionTypeAdapter<T, E>(
//为什么Java可以直接写typeToken: TypeToken<T>,kotlin不行?
private val elementTypeAdapter: TypeAdapter<T>, private val typeToken: TypeToken<E>
) : TypeAdapter<Collection<T>>() { override fun write(out: JsonWriter, value: Collection<T>?) {
if (value == null) {
out.nullValue()
return
}
out.beginArray()
for (element in value) {
elementTypeAdapter.write(out, element)
}
out.endArray()
} override fun read(jsonReader: JsonReader): Collection<T> {
val constructorConstructor = ConstructorConstructor(HashMap())
val constructor = constructorConstructor.get(typeToken)
as ObjectConstructor<out Collection<T>> if (jsonReader.peek() == JsonToken.NULL) {
jsonReader.nextNull()
//return emptyList()
//这样写会报错:field language has type java.util.ArrayList, got kotlin.collections.EmptyList,当ListBean为ArrayList而不是List时会报错
//return mutableListOf()
return constructor.construct()
}
val collection: MutableCollection<T> = constructor.construct() as MutableCollection<T> jsonReader.beginArray()
while (jsonReader.hasNext()) {
val instance: T = elementTypeAdapter.read(jsonReader)
collection.add(instance)
}
jsonReader.endArray()
return collection
}
}

与2.1创建的TypeAdapter相比,最大的不同有如下几点

  1. read()write()方法对数据处理,需要使用jsonReader.beginArray()开始,jsonReader.endArray()结束
  2. 构造方法里需要传一个类型为TypeAdapter的elementTypeAdapter对象,供read()write()方法读取和写入使用

write()

  1. 因为我们集合内的数据可能为各种类型,所以我们不能再像2.1中那样使用out.value(value),该方法只能传入String类型,而TypeAdapter的write()方法解释如下,可以写入各种类型,所以我们使用传来的elementTypeAdapter对象的这个方法来写入集合内的各个元素。
Writes one JSON value (an array, object, string, number, boolean or null)for {@code value}.

read()

  1. 当集合字段返回null时,我们需要返回空集合,这里我们先通过ConstructorConstructor的get()方法创建TypeToken类型的ObjectConstructor对象,然后使用ObjectConstructor的construct()方法来创建Collection集合,而不是return emptyList(),若字段是ArrayList类型而不是List时,使用return emptyList()就会报错
field com.myfittinglife.typeadapterdemo.entities.ListBean.language has type java.util.ArrayList, got kotlin.collections.EmptyList
  1. 读取时,我们也是使用传来的elementTypeAdapter对象的read()方法来读取该集合内的各个元素数据。

3.2 创建TypeAdapterFactory

class MyTypeAdapterFactory : TypeAdapterFactory {

    override fun < T> create(gson: Gson, typeToken: TypeToken<T>): TypeAdapter<T>? {
val rawType = typeToken.rawType
if (rawType == String::class.java) {
return StringTypeAdapter() as TypeAdapter<T>
}
if (rawType == Point::class.java) {
return PointTypeAdapter() as TypeAdapter<T>
}
//测试Collection.class.isAssignableFrom(rawType)
//不用rawType==Collection::class.java,因为集合可能为ArrayList、List、或者其它继承自List的类型,写==就定死了,这个isAssignableFrom()
//方法表示该对象表示的类或接口与指定参数表示的类或接口相同,或其超类或超接口相同,则返回true
if (Collection::class.java.isAssignableFrom(rawType)) {
//获取底层实例
val type: Type = typeToken.type
//获取此集合内的元素类型
val elementType: Type = `$Gson$Types`.getCollectionElementType(type, rawType)
//生成该元素类型的TypeAdapter
val elementTypeAdapter = gson.getAdapter(TypeToken.get(elementType)) //----
return CollectionTypeAdapter(elementTypeAdapter, typeToken) as TypeAdapter<T>
}
return null
}
}
  1. 这里判断是集合类型,没有像上面用到rawType==Collection::class.java来判断,而是通过isAssignableFrom()方法,因为集合可能为ArrayList、List、或者其它继承自List的类型,写==就定死了,这个isAssignableFrom()方法表示该对象表示的类或接口与指定参数表示的类或接口相同,或其超类或超接口相同,则返回true,所以用这个来判断是否是集合类型比较好。

  2. 3.1小结TypeAdapter的构造方法内需要我们传入一个集合内元素的TypeAdapter,这里通过如下方式获取

//获取底层实例
val type: Type = typeToken.type
//获取此集合内的元素类型
val elementType: Type = `$Gson$Types`.getCollectionElementType(type, rawType)
//生成该元素类型的TypeAdapter
val elementTypeAdapter = gson.getAdapter(TypeToken.get(elementType))
return CollectionTypeAdapter(elementTypeAdapter, typeToken) as TypeAdapter<T>

3.3 测试使用

其它步骤和2.3和2.4相同,这里我们直接测试使用

data class ListBean(val name: String, val language: List<String>)

data class ListBean(val name: String, val language: ArrayList<String>)

data class ListBean(val name: String, val language: Set<String>)
等...

字符串转Object

这里我们解析字符串{"language":null,"name":"李华"},字段language集合为null

val str = "{\"language\":null,\"name\":\"李华\"}"
val myGson = GsonBuilder().registerTypeAdapterFactory(MyTypeAdapterFactory()).create()
val typeAdapterBean = myGson.fromJson<ListBean>(str, ListBean::class.java)
Log.i(TAG, "typeAdapterBean: $typeAdapterBean")

打印的结果如下

ListBean(name=李华, language=[])

可以看到, 我们解析时将null直接转为了[]

Object转字符串

val listBean = ListBean("张帅", listOf())
//或val listBean = ListBean("张帅", setOf())
val str = myGson.toJson(listBean)
Log.i(TAG, "str:$str")

打印的结果如下

str:{"language":[],"name":"张帅"}

3.4 疑问

其实在TypeAdapter的构造方法内还是有点疑惑,为什么这里不能直接使用泛型T呢?如果直接写泛型T,

在TypeAdapterFactory内创建TypeAdapter时,就会报错

 override fun < T> create(gson: Gson, typeToken: TypeToken<T>): TypeAdapter<T>? {
.....
return CollectionTypeAdapter(elementTypeAdapter, typeToken) as TypeAdapter<T>
}

错误提示

Type mismatch.
Required: T Found: Any!
Type mismatch.
Required: CapturedType(out (kotlin.Any..kotlin.Any?)) Found: T

这里的参数typeToken本身和返回的TypeAdapter不是一样的是泛型T吗?为什么编辑器会显示编译错误?还请知道的大神指教下,不胜感激。

4 总结

  1. 想要统一处理字段为String但值为null的数据,可以使用TypeAdapter对其进行统一处理
  2. 若想处理集合为空不传[],但传的是null的情况,也可以用这种方法
  3. 对多种类型处理的多个TypeAdapter,只需在TypeAdapterFactory对不同类型使用不同TypeAdapter即可

举一反三,如果出现String字段传来Int的错误、List内的元素可能传各种类型、还有.....(怒气暴增)不说了,哪有这种不规范的接口, 我去打后端去了!!!

以上就是全部内容,如果本文对你有帮助,请别忘记点赞start,如果有不恰当的地方也请提出来,下篇文章见。

Github

参考文章

使用gson将null String对象转换成空白字符串

Kotlin Json库问题总结(Gson以及Moshi的坑)

TypeAdapter处理Gson解析,null值替换为"",null值替换为[]的更多相关文章

  1. 我的Android进阶之旅------>解决Jackson、Gson解析Json数据时,Json数据中的Key为Java关键字时解析为null的问题

    1.问题描述 首先,需要解析的Json数据类似于下面的格式,但是包含了Java关键字abstract: { ret: 0, msg: "normal return.", news: ...

  2. Gson 解析教程

    Gson 是google解析Json的一个开源框架,同类的框架fastJson,JackJson等等 本人fastJson用了两年,也是从去年才开始接触Gson,希望下面的总结会对博友有用,至于Gso ...

  3. 【转】采用Gson解析含有多种JsonObject的复杂json

    本文对应的项目是MultiTypeJsonParser ,项目地址 https://github.com/sososeen09/MultiTypeJsonParser 0 前奏 使用 Gson 去解析 ...

  4. Android网络之数据解析----使用Google Gson解析Json数据

    [声明] 欢迎转载,但请保留文章原始出处→_→ 生命壹号:http://www.cnblogs.com/smyhvae/ 文章来源:http://www.cnblogs.com/smyhvae/p/4 ...

  5. Gson解析空字符串异常的处理

    面对一些不规范的json,我们的gson解析经常会抛出各种异常导致app崩溃,这里可以采取一些措施来避免. 我们期望在后台返回的json异常时,也能解析成功,空值对应的转换为默认值,如:newsId= ...

  6. Gson解析第三方提供Json数据(天气预报,新闻等)

    之前都是自己写后台,自己的server提供数据给client. 近期在看第三方的数据接口,訪问其它站点提供的信息.比方.我们可能自己收集的数据相当有限.可是网上提供了非常多关于天气预报.新闻.星座运势 ...

  7. 通过Gson解析Json数据

    Json是一种数据格式,便于数据传输.存储.交换:Gson是一种组件库,可以把java对象数据转换成json数据格式. gson.jar的下载地址:http://search.maven.org/#s ...

  8. Android解析Json数据之Gson解析

    Gson是谷歌官方提供的解析json数据的工具类.json数据的解析能够使用JSONObject和JSONArray配合使用解析数据,可是这样的原始的方法对于小数据的解析还是有作用的,可是陪到了复杂数 ...

  9. Android中使用Gson解析JSON数据的两种方法

    Json是一种类似于XML的通用数据交换格式,具有比XML更高的传输效率;本文将介绍两种方法解析JSON数据,需要的朋友可以参考下   Json是一种类似于XML的通用数据交换格式,具有比XML更高的 ...

  10. 【框架】网络请求+Gson解析--Retrofit 2

    其实内部是封装了Okhttp和Gson解析 public class CourseFragmentAPI { public static void get(String userId, BaseCal ...

随机推荐

  1. 系列解读 SMC-R (二):融合 TCP 与 RDMA 的 SMC-R 通信 | 龙蜥技术

    ​简介:本篇以 first contact (通信两端建立首个连接) 场景为例,介绍 SMC-R 通信流程. ​ 文/龙蜥社区高性能网络SIG 一.引言 通过上一篇文章 <系列解读SMC-R:透 ...

  2. 小米电商 Apache Dubbo-go 微服务实践

    ​简介:2021 年是小米中国区电商部门变动调整较大的一年,小米中国区早期电商.服务体系建立在 Go 语言构建的微服务体系之上,由内部自研的 Go 语言微服务框架 koala 支撑起数以千计的微服务应 ...

  3. ARMS企业级场景被集成场景介绍

    简介: ARMS企业级场景被集成场景介绍 通过本次最佳实践内容,您可以看到ARMS OpenAPI可以灵活的被集成到客户链路监控场景,并对其进行可视化图形展示监控信息. 1. 背景信息 应用实时监控服 ...

  4. [GPT] export, export default, import, module.exports, require

    ES6 规范:export 和 import 配对 import 的 {} 大括号里面指定要从其他模块导入的变量名, 如果 export 命令没有写 default,那么 import {} 大括号里 ...

  5. win10 uwp 简单制作一个 Path 路径绘制的图标按钮

    本文告诉大家在 UWP 或 WinUI 3 里面如何简单制作一个由 Path 几何路径图形绘制的图标按钮 先在资源里面定义按钮的样式,重写 Template 属性,通过在 Template 里面放入 ...

  6. Jetpack Compose(6)——动画

    目录 一.低级别动画 API 1.1 animate*AsState 1.2 Animatable 1.3 Transition 动画 1.3.1 updateTransition 1.3.2 cre ...

  7. SpringMVC学习二(日期参数/数据保存/重定向)

    接受的参数为日期类型 controller进行数据保存 Controller如何进行重定向跳转 1.对于前端页面传来日期类型的数据时如何进行处理,有两种方法 1.1在对应的Controller中插入代 ...

  8. goframe v2.1.0 gf-cli的使用

    目录 1.视频教程 2.官方文档 3.下载 linux系统安装环境 windows系统安装环境 4.创建项目 5.启动项目 6.交叉编译 7.gen命令的使用 8.orm的操作 1.视频教程 http ...

  9. 🔥🔥httpsok-谷歌免费SSL证书如何申请

    httpsok-谷歌免费SSL证书如何申请 使用场景: 部署CDN证书.OSS云存储证书 证书类型: 单域名 多域名 通配符域名 混合域名 证书厂商: ZeroSSL Let's Encrypt Go ...

  10. Netflow/IPFIX 流量收集与分析

    目录 文章目录 目录 Netflow(网络数据流检测协议) IPFIX(网络流量监测) IPFIX 组网架构 IPFIX 应用场景 Usage-based Accounting(基于使用流量的计费) ...