前言

在与后端对接过程中,常常会出现因为后端不规范,导致某些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. [Trading] 股票日内交易者能赚多少钱 - 看到日内交易的潜力并学习如何实现它

    无论是为了生活方式.寻求刺激还是为了挑战,股票市场短线交易者能赚多少钱的问题不可避免地出现了. 股票日内交易者的交易量变化很大,一些当日交易者损失了他们的资金,而另一些人利用他们的资金创造了一个高的月 ...

  2. [Go] 浅谈 gorm 执行 AutoMigrate 的两种时机

    第一种就是直接在操作 model 的逻辑中,执行 db.AutoMigrate,模型没有更新时不会有 schema 相关的 sql 被执行. 第二种就是单独定义一个属于 main 包的 go 文件,专 ...

  3. WPF 使用 Skia 解析绘制 SVG 图片

    本文告诉大家如何在 WPF 里面,使用 Skia 解析绘制 SVG 图片.本文也适合控制台使用 SkiaSharp 解析绘制 SVG 图片,本文的 WPF 部分只是在 Skia 绘制完成之后,将 Sk ...

  4. 局域网内一部分网络设备无法ping通,icmp_seq=1 目标主机不可达

    问题: 来自 192.168.2.99 icmp_seq=1 目标主机不可达.   最近想在局域网内搭建一台服务器,打开SSH服务后发现局域网内的一部分设备无法使用,尝试了各种办法都没能解决,重装系统 ...

  5. Modelsim使用指南

    Modelsim使用指南 本文讲述Modelsim的使用步骤. 添加一个测试文件,比如modulename.v. 编辑这个Verilog模块. 为了方便讲述,顶层模块名命名为"top&quo ...

  6. 云原生最佳实践系列 7:基于 OSS Object FC 实现非结构化文件实时处理

    01 方案概述 现在绝大多数客户都有很多非结构化的数据存在 OSS 中,以图片,视频,音频居多.举一个图片处理的场景,现在各种终端种类繁多,不同的终端对图片的格式.分辨率要求也不同,所以一张图片往往会 ...

  7. LLM优化:开源星火13B显卡及内存占用优化

    1. 背景 本qiang~这两天接了一个任务,部署几个开源的模型,并且将本地经过全量微调的模型与开源模型做一个效果对比. 部署的开源模型包括:星火13B,Baichuan2-13B, ChatGLM6 ...

  8. uniapp中OnShow获取缓存

    点击查看代码 function getStorage(key) { return new Promise((resolve, reject) => { uni.getStorage({ key: ...

  9. IPv6 — 基于邻居发现协议的通信方式

    目录 文章目录 目录 前文列表 IPv6 的通信方式 NDP(Neighbor Discovery Protocol,邻居发现协议) IPv6 地址自动配置 无状态自动配置概述 前文列表 <IP ...

  10. C#.Net筑基-类型系统①基础

    C#.Net的BCL提供了丰富的类型,最基础的是值类型.引用类型,而他们的共同(隐私)祖先是 System.Object(万物之源),所以任何类型都可以转换为Object. 01.数据类型汇总 C#. ...