在API请求/响应中使用空字符串,null或删除空属性


25

通过API传输对象时(如无模式JSON格式),返回不存在的字符串属性的理想方法是什么?我知道这样做的方式有很多,如下面列出的链接中的示例所示。

我确定我过去曾经使用过null,但是没有足够的理由这样做。在处理数据库时,直接使用null似乎很简单。但是数据库似乎是一个实现细节,不应与API另一方有关。例如,他们可能使用无模式数据存储,该模式仅存储具有值的属性(非null)。

从代码的角度来看,将字符串函数限制为仅适用于一种类型(即string(非null))可以使它们更易于证明;避免null也是拥有Option对象的原因。因此,如果产生请求/响应的代码不使用null,那么我猜想API另一侧的代码也不会被强制使用null。

我更喜欢使用空字符串作为避免使用null的简便方法的想法。我听说过使用null并针对空字符串的一个论点是,空字符串表示该属性存在。尽管我理解了区别,但我也想知道这是否只是实现细节,使用null或空字符串是否会在实际生活中带来任何区别。我也想知道空字符串是否类似于空数组。

那么,解决这些问题的最佳方法是什么?它是否取决于要传输的对象的格式(schema / schemaless)?


2
还要注意,Oracle以相同的方式对待空字符串和空字符串。正确的是:在纸质问卷上,您如何区分未给出答案和由空字符串组成的答案?
伯恩哈德·希勒

如果使用继承,则很容易说。if( value === null) { use parent value; } 但是,如果将子值设置为空字符串(例如,使用空白覆盖默认父值),那么如何“重新继承”该值?对我来说,将其设置为null意味着“未设置此值,因此我们知道使用父值”。
弗兰克·福尔泰

由于“删除空属性”也是为什么要“避免” a的原因null(确实是这样null避免的),因此发问者在他们使用“返回非空” [Object](即:空字符串,空数组等)时表示“写“避免”。
cellepo

Answers:


18

TLDR;删除空属性

首先要记住的是,应用程序的边缘不是面向对象的(如果在该范式中进行编程,则不会起作用)。您收到的JSON不是对象,因此不应将其视为对象。只是结构化的数据可能会(也可能不会)转换为对象。通常,在对传入的JSON进行身份验证之前,不应将其视为业务对象。反序列化的事实并不能使其有效。由于JSON与后端语言相比还具有有限的原语,因此为传入数据制作JSON对齐的DTO通常是值得的。然后使用DTO构造业务对象(或尝试错误)以运行API操作。

当您将JSON视为一种传输格式时,省略未设置的属性会更有意义。通过电线发送的费用更少。如果您的后端语言默认情况下不使用空值,则可以将反序列化器配置为产生错误。例如,我对Newtonsoft.Json的通用设置仅将null / missing属性option仅与F#类型转换,否则将出错。这自然表示了哪些字段是可选的(带有option类型的字段)。

与往常一样,概括只能使您走到目前。在某些情况下,默认或null属性更合适。但是关键不在于将系统边缘的数据结构视为业务对象。成功创建业务对象时,应带有业务保证书(例如,名称至少3个字符)。但是,脱离网络的数据结构并没有真正的保证。


3
尽管大多数现代的串行器都有可选字段,但是从响应中省略空值并不总是一个好主意,因为它可能会引入额外的复杂性来处理可空字段。因此,真正的情况下,依赖,取决于您的序列库处理nullables如何,以及是否或不处理这些nullables(潜在)额外的复杂性是真正值得每一个请求节约的几个字节。您必须努力分析业务案例。
克里斯·西里菲斯

@ChrisCirefice是的,我相信最后一段涵盖了这一点。在某些情况下,采用不同的策略会更好。
Kasey Speakman

我同意JSON仅用作传输格式,它不会通过CORBA之类的电线传递对象。我也同意可以添加和删除属性。表示形式可以更改,控件可以更改,尤其是在Web上。
imel96

15

更新:我对答案做了一些编辑,因为这可能会引起混乱。


使用空字符串是肯定的。空字符串仍然是一个值,只是空的。不应使用不代表任何值的构造来指示任何值null

从API开发人员的角度来看,仅存在两种类型的属性:

  • 必填项(这些值必须具有其特定类型的值,并且绝不能为空),
  • 可选(这些MAY可以包含其特定类型的值,但MAY还可包含null

这很清楚地说明了何时属性是强制性的。必不可少null

另一方面,如果未设置对象的可选属性并将其保留为空,则无论如何,我还是希望将它们保留在响应中并带有该null值。根据我的经验,由于API客户端不需要检查属性是否确实存在,因为它们一直存在,因此使API客户端更容易实现解析,并且可以将响应简单地转换为自定义DTO,从而处理null值作为可选。

动态包括/从响应力中删除字段,包括客户端上的其他条件。


无论选择哪种方式,请确保您保持一致并有据可查。这样,只要行为是可预测的,使用API​​的大小实际上就无关紧要。


是的,空字符串是一个值,我使用它null作为参考。将值与引用混合在一起是我关注的问题之一。如何将可选字段与null具有null值的非可选字符串字段区分开?重新解析,不是测试属性的存在会使解析器更加脆弱吗?
imel96 '17

2
@ imel96非可选字段永远不能为null。如果某些内容是非可选的,则必须始终包含一个值(其特定类型)。
安迪

3
这个。作为API的经常使用者,我讨厌不得不处理“动态”结构,即使它以可选字段的形式被忽略了。(也普遍认为ZLS和Null之间有很大的差异)。我会整天高兴地接受null值。作为API的作者,我的目标之一是使客户端的使用尽可能轻松,这意味着始终具有预期的数据结构。
jleach

@DavidPacker因此,如果我理解正确,则使用null该值指示可选值。因此,当您定义一个具有非可选字符串属性的对象,而使用者没有此属性时,它必须为该属性发送空字符串。那正确吗?
imel96 '17

2
@GregoryNisbet请不要这样做。这是没有意义的。
安迪

3

null 用法取决于应用程序/语言

最终,是否null用作有效的应用程序值的选择很大程度上取决于您的应用程序和编程语言/接口/边缘。

从根本上讲,如果值的类别不同,我建议尝试使用不同的类型。null如果您的界面允许,并且您要表示的属性只有两个类,则可以选择。如果界面或格式允许,则可以忽略属性。新的聚合类型(类,对象,消息类型)可能是另一种选择。

对于您的字符串示例,如果这是用编程语言编写的,我会问自己几个问题。

  1. 我打算增加将来的价值类型吗?如果是这样,那么Option对于您的界面设计来说可能会更好。
  2. 我什么时候需要验证消费者电话?静态地?动态地?之前?后?都没有 如果您的编程语言支持,请使用静态类型的好处,因为它避免了为验证而必须创建的代码量。Option如果您的字符串不可为空,则可能最适合此情况。但是,null无论如何,您仍然可能必须检查用户输入的字符串值,因此我可能会推迟到第一行提问:我想/将要表示多少种类型的值。
  3. 是否null显示我的编程语言中的程序员错误?不幸的是,null在某些语言中,通常是未初始化(或未明确初始化)的指针或引用的默认值。是null因为作为默认值可接受的值?它是安全的默认值?有时null表示已释放的值。我是否应该为接口的使用者提供程序中潜在的内存管理或初始化问题的指示?面对此类问题,此类呼叫的失败模式是什么?调用者是否与我的线程处于同一进程或线程中,从而使此类错误对我的应用程序构成高风险?

根据您对这些问题的回答,您可能可以磨练是否null适合您的界面。

例子1

  1. 您的应用程序对安全至关重要
  2. 您在启动时使用某种类型的堆初始化,并且null是在无法为字符串分配空间时可能返回的可能的字符串值。
  3. 这样的字符串可能会击中您的界面

答:null可能不合适

基本原理:null在这种情况下,实际上是用来指示两种不同类型的值。第一个可能是您的界面用户可能想要设置的默认值。不幸的是,第二个值是一个标志,指示您的系统运行不正常。在这种情况下,您可能希望尽可能安全地失败(无论对您的系统意味着什么)。

例子2

  1. 您正在使用具有char *成员的C结构。
  2. 您的系统不使用堆分配,而是在使用MISRA检查。
  3. 您的接口接受此结构作为指针,并检查以确保该结构未指向 NULL
  4. char *API成员的默认值和安全值可以由单个值表示NULL
  5. 在用户的结构初始化后,您希望为用户提供不显式初始化char *成员的可能性。

答:NULL可能合适

基本原理:您的结构通过NULL检查但未初始化的可能性很小。但是,除非您对结构值进行某种校验和和/或对结构地址进行范围检查,否则您的API可能无法解决这一问题。MISRA-C linter可以通过在初始化之前标记结构的使用来帮助您的API用户。但是,对于char *成员,如果指向struct的指针指向已初始化的struct,NULL则为struct初始化程序中未指定成员的默认值。因此,它NULL可以作为char *应用程序中struct成员的安全默认值。

如果它在序列化接口上,我会问自己以下问题,有关是否在字符串上使用null。

  1. 是否null表明潜在的客户端错误?对于JavaScript中的JSON,这可能是一个“否”,null并不一定表示分配失败。在JavaScript中,它被用作指示要设置成问题的引用中对象缺失的明确指示。但是,有一些非JavaScript解析器和序列化器可将JSON映射null到本机null类型。如果是这种情况,那么就开始讨论null您的特定语言,解析器和序列化器组合是否可以使用本机用法。
  2. 明确缺少属性值是否会比单个属性值影响更多?有时,a null实际表示您完全具有新的消息类型。仅指定完全不同的消息类型,对于使用序列化格式的用户来说可能更干净。这确保了它们的验证和应用程序逻辑可以将Web界面提供的消息的两种区别完全分开。

一般建议

null不能是不支持边缘或接口的值。如果您使用的属性值类型(例如JSON)的输入极其松散,请尝试在使用者边缘软件(例如JSON Schema)上推送某种形式的模式或验证。如果它是一种编程语言API,则在可能的情况下(通过键入)静态验证用户输入,或者在运行时尽可能合理地验证用户输入(也可以在面向消费者的界面上进行防御性编程)。重要的是,记录或定义边,因此毫无疑问:

  • 给定属性接受什么类型的值
  • 哪些值范围对于给定属性有效。
  • 聚合类型应如何构造。聚合类型必须/应该/可以存在哪些属性?
  • 如果是某种类型的容器,那么该容器可以容纳或应该容纳多少个项目,以及该容器可以容纳哪些值?
  • 容器或集合类型的属性或实例返回什么顺序(如果有)?
  • 设置特定值有什么副作用?读取这些值有什么副作用?

1

这里我对这些问题进行个人分析。它没有任何书籍,论文,研究或其他任何东西的支持,只是我的亲身经历。

空字符串为 null

对我来说这是不行的。不要将空字符串的语义与未定义的语义混合使用。在许多情况下,它们可能是完全可互换的,但是您可能会遇到未定义和已定义但为空的含义确实有所不同的情况。

一种愚蠢的示例:假设有一个存储外键的属性,并且该属性未定义或为null,这意味着未定义任何关系,而空字符串""可以理解为已定义的关系,并且外部记录的ID是该空字符串。

未定义vs null

这不是一个黑色或白色的话题。两种方法都各有利弊。

支持显式定义null值,这些优点如下:

  • 消息更具描述性,因为您只需查看任何消息即可了解所有键
  • 与上一点有关,更容易编写代码并检测数据使用方中的错误:如果获取了错误的键(例如拼写错误,API可能已更改等),则更容易检测错误。

支持假设不存在的键等于null

  • 一些更改更容易适应。例如,如果消息模式的新版本包含新密钥,则即使消息的生产者尚未更新并且尚不提供此信息,也可以对信息的使用者进行编码以使用此将来的密钥。
  • 消息可以不那么冗长或更短

如果API某种程度上是稳定的,并且您已对其进行了详细记录,那么我认为声明不存在的键等于的含义是完全可以的null。但是,如果它更加混乱和混乱(通常是这样),那么我认为如果在每条消息中明确定义每个值,就可以避免麻烦。即,如果有疑问,我倾向于遵循冗长的方法。

综上所述,最重要的是:清楚陈述自己的意图并保持一致。不要在这里做一件事情,在那儿做另一件事情。可预测的软件是更好的软件。


我使用空字符串的示例就是实现细节,即假设使用API​​公开数据库行。如果不涉及数据库,而仅用于传输对象表示形式,这会有所不同吗?
imel96 '17

它不一定是实现细节。我的示例实际上讨论了与数据库相关的PK,但是我试图解释的是一个空字符串不是nil / nothing / null。另一个示例:在游戏中,有一个角色对象,并且具有“伙伴”属性。一个null合作伙伴显然意味着在所有没有合作伙伴,但""可以理解为有一个合作伙伴,它的名字叫""
bgusach

我对空伙伴引用没问题,这意味着没有伙伴并且引用也不是字符串。但是合作伙伴名称是一个字符串,即使您允许使用null作为合作伙伴名称,您是否也不会在某个时候捕获该null并将其替换为空字符串?
imel96 '17

如果没有伙伴,则不会更改null为空字符串。也许以某种形式呈现它,但是从来没有在数据模型中呈现。
bgusach

我并不是说没有伙伴,伙伴将成为对象。name我在说的是合作伙伴,您可以允许合作伙伴名称为空吗?
imel96 '17

1

在存在字符串的情况下,我会提供一个空字符串,而它恰好是一个空字符串。在我要明确地说“不,该数据不存在”的情况下,我将提供null。并省略说“没有数据,不要打扰”的键。

您判断哪些情况可能发生。空字符串对您的应用程序有意义吗?您要区分使用null显式表示“无数据”和不具有值隐式表示吗?如果客户端需要区分这两种可能性,则应该只有这两种可能性(无密钥且无密钥)。

现在请记住,这全都与传输数据有关。接收者对数据的处理是他们的业务,他们将做对他们来说最方便的事情。接收器应该能够处理您扔给它的任何东西(可能通过拒绝数据)而不会崩溃。

如果没有其他考虑,那么我将发送对发件人最方便的内容并记录下来。我不希望根本不发送缺少的值,因为这可能会提高编码,传输和解析JSON的速度。


我喜欢“如果需要由客户区分”的观点。
imel96 '17

0

尽管我不能说什么是最好的,但几乎可以肯定这不是一个简单的实现细节,它改变了如何与该变量交互的结构。

如果某些内容可以为null,则应始终将其视为在某点上为null,因此您将始终具有两个工作流程,一个为null,一个为有效字符串。拆分工作流不一定是一件坏事,因为您可以利用很多错误处理和特殊情况,但这确实会使您的代码模糊。

如果您始终以相同的方式与字符串交互,则该功能可能更容易保留在您的脑海中

因此,与任何“什么是最好的”问题一样,我留下的答案是:取决于。如果要拆分工作流程并在未设置任何内容时更明确地捕获,请使用null。如果您希望程序仅继续执行操作,请使用空字符串。重要的是您要保持一致,选择共同的回报并坚持下去。

考虑到您正在创建API,我建议您坚持使用空字符串,因为用户需要补偿的内容较少,因为作为API用户,我不知道您的API可能会给我空值的所有原因,除非您很好地记录了下来,有些用户还是不会阅读。


具有“拆分工作流”是不好的。可以说,在生产者端,一切都是干净的,字符串类型方法仅返回strings,从不返回null。如果API使用null,则生产者有时需要创建此值null以符合API。然后,消费者也需要处理null。但是我想我明白您的意思,只是通过授权来决定和定义API,对吗?这是否意味着它们中的任何一个都没有错?
imel96 '17

是的,您在API中所做的任何事情都会影响用户构造代码的方式,因此从API用户的角度考虑您的设计,您应该能够确定哪种方式是最佳的。最终,这是您的API。保持一致。只有您可以决定采用这种方法的利弊。
Erdrik Ironrose'Mar

0

文献!

TL; DR>

按您认为合适的方式进行-有时使用它的上下文很重要。示例,将变量绑定到Oracle SQL:空字符串被解释为NULL。

我只想简单地说-确保记录所提到的每种情况

  • 空值
  • 空白(空)
  • 丢失(已删除)

您的代码可能以不同的方式起作用-记录您的代码如何对其做出反应:

  • 失败(异常等),甚至可能无法通过验证(可能是已检查的异常)而无法正确处理这种情况(NullPointerException)。
  • 提供合理的默认值
  • 代码的行为有所不同

除此之外,您还需要始终保持一致的行为,并可能采用一些自己的最佳实践。记录一致的行为。例子:

  • 对待Null和Missing相同
  • 完全照此处理空字符串。仅在使用SQL绑定的情况下,才可以将其视为空白。确保您的SQL行为一致且符合预期。

问题是,在不解决问题的情况下,分歧经常发生。考虑在团队环境中,决策必须是团队决策,很多时候这意味着会有争论。当您有多个团队时,每个团队都有权自行决定。我见过的API我只能猜测它们是由彼此不同意的不同团队实现的。如果任何人都可以同意一件事,则记录下来是微不足道的。
imel96 '17

0

tl; dr-如果使用它:含义一致。

如果包括在内null,这意味着什么?事物的宇宙意味着什么。一个值根本不足以表示缺失或未知的值(而这仅是众多可能性中的两种:例如,缺失-已被测量,但我们尚不知道。未知-我们未尝试测量它。)

在我最近遇到的一个示例中,一个字段可能是空的,因为未报告该字段是为了保护某人的隐私,但是在发件人一方是已知的,在发件人一方却未知,但是原始报告者知道该字段,或者两者都不知道。所有这些都与接收者有关。因此,通常一个值是不够的。

在开放世界的假设下(您根本不了解未说明的事情),您只需将其忽略即可。使用封闭世界假设(未声明的事实是错误的,例如在SQL中),您可以更好地弄清null含义,并尽可能与该定义保持一致...

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.