Usa stringa vuota, null o rimuovi la proprietà vuota nella richiesta / risposta API


25

Quando si trasferisce un oggetto tramite un'API, come nel formato JSON senza schemi, qual è il modo ideale per restituire proprietà di stringa inesistenti? So che ci sono diversi modi per farlo, come negli esempi nei link elencati di seguito.

Sono sicuro di aver usato null in passato, ma non ho una buona ragione per farlo. Sembra semplice usare null quando si ha a che fare con il database. Ma il database sembra un dettaglio di implementazione che non dovrebbe riguardare la parte dall'altra parte dell'API. Ad esempio, probabilmente usano un archivio dati senza schemi che memorizza solo proprietà con valori (non nulli).

Da un punto di vista del codice, limitare le funzioni di stringa a lavorare solo con un tipo, ovvero string(non null), le rende più facili da provare; evitare null è anche un motivo per avere Optionoggetto. Quindi, se il codice che produce richiesta / risposta non usa null, la mia ipotesi è che il codice sull'altro lato dell'API non sarà costretto a usare anche null.

Mi piace piuttosto l'idea di usare una stringa vuota come un modo semplice per evitare di usare null. Un argomento che ho sentito per l'uso di null e contro la stringa vuota è che la stringa vuota significa che la proprietà esiste. Anche se capisco la differenza, mi chiedo anche se sia il giusto dettaglio di implementazione e se l'uso di una stringa nulla o vuota fa la differenza nella vita reale. Mi chiedo anche se una stringa vuota sia analoga a una matrice vuota.

Quindi, qual è il modo migliore per farlo che affronta queste preoccupazioni? Dipende dal formato dell'oggetto da trasferire (schema / schema)?


2
Si noti inoltre che Oracle tratta le stringhe vuote e le stringhe null allo stesso modo. E proprio ci sono: su un questionario cartaceo, come potresti distinguere tra nessuna risposta data e una risposta costituita da una stringa vuota?
Bernhard Hiller

Se si utilizza l'ereditarietà, è facile dire if( value === null) { use parent value; } Tuttavia, se si imposta il valore figlio, anche su una stringa vuota (ad es. Sovrascrivere il valore genitore predefinito con vuoto), come si "eredita nuovamente" il valore? Per me, impostarlo come null significherebbe "disinserire questo valore in modo che sappiamo usare il valore genitore".
Frank Forte,

Poiché "Rimuovi proprietà vuote" è anche il motivo per "evitare" un null(è vero che nullè evitato in quanto tale), l'interrogante significa "Restituisce un oggetto nullo" (ovvero: stringa vuota, matrice vuota, ecc.) Quando scrivi "evitare".
cellepo,

Risposte:


18

TLDR; Rimuovi proprietà null

La prima cosa da tenere a mente è che le applicazioni ai loro margini non sono orientate agli oggetti (né funzionali se programmano in quel paradigma). Il JSON che ricevi non è un oggetto e non deve essere trattato come tale. Sono solo dati strutturati che possono (o non possono) convertirsi in un oggetto. In generale, nessun JSON in entrata deve essere considerato affidabile come oggetto business fino a quando non viene convalidato come tale. Solo il fatto che sia stato deserializzato non lo rende valido. Poiché JSON ha anche primitive limitate rispetto ai linguaggi di back-end, spesso vale la pena creare un DTO allineato a JSON per i dati in arrivo. Quindi utilizzare DTO per costruire un oggetto business (o errore durante il tentativo) per eseguire l'operazione API.

Quando si considera JSON solo come un formato di trasmissione, ha più senso omettere le proprietà non impostate. È meno per inviare attraverso il filo. Se la lingua del back-end non utilizza valori nulli per impostazione predefinita, è possibile che sia possibile configurare il deserializzatore in modo che fornisca un errore. Ad esempio, la mia configurazione comune per Newtonsoft.Json traduce le proprietà null / mancanti solo da / verso i optiontipi F # e altrimenti commetterà un errore. Ciò fornisce una rappresentazione naturale di quali campi sono opzionali (quelli con optiontipo).

Come sempre, le generalizzazioni ti portano solo così lontano. Probabilmente ci sono casi in cui una proprietà predefinita o null si adatta meglio. Ma la chiave non è guardare alle strutture di dati ai margini del sistema come oggetti business. Gli oggetti business devono avere garanzie commerciali (es. Nome di almeno 3 caratteri) se creati con successo. Ma le strutture dati estratte dal filo non hanno garanzie reali.


3
Sebbene la maggior parte dei serializzatori moderni disponga di campi opzionali, omettere i null dalla risposta non è sempre una buona idea, perché può introdurre ulteriore complessità per gestire i campi nullable. Pertanto, dipende davvero dal maiuscolo / minuscolo , a seconda di come la libreria di serializzazione gestisce nullable e se la (potenziale) ulteriore complessità della gestione di tali nullable valga davvero la pena di risparmiare qualche byte per richiesta. Devi lavorare sodo per analizzare i tuoi casi aziendali.
Chris Cirefice,

@ChrisCirefice Sì, credo che l'ultimo paragrafo lo riguardi. Ci sono casi in cui sarà meglio impiegare strategie diverse.
Kasey Speakman

Concordo sul fatto che JSON viene utilizzato solo come formato di trasmissione, non passa oggetti attraverso i fili come CORBA. Sono anche d'accordo che le proprietà possono essere aggiunte e rimosse; le rappresentazioni possono cambiare, i controlli possono cambiare, specialmente sul web.
imel96,

15

Aggiornamento: ho modificato un po 'la risposta, perché potrebbe aver creato confusione.


Andare con una stringa vuota è un no definitivo. La stringa vuota è ancora un valore, è solo vuota. Nessun valore deve essere indicato utilizzando un costrutto che rappresenta nulla, null.

Dal punto di vista dello sviluppatore API, esistono solo due tipi di proprietà:

  • richiesto (questi DEVONO avere un valore del loro tipo specifico e NON DEVONO mai essere vuoti),
  • facoltativo (questi possono contenere un valore del loro tipo specifico ma possono anche contenere null.

Questo rende abbastanza chiaro che quando una proprietà è obbligatoria, ad es. richiesto, non può mai essere null.

D'altra parte, se una proprietà opzionale di un oggetto non fosse impostata e lasciata vuota, preferisco tenerli nella risposta comunque con il nullvalore. In base alla mia esperienza, per i client API è più semplice implementare l'analisi, in quanto non sono tenuti a verificare se una proprietà esiste o meno, perché è sempre presente e possono semplicemente convertire la risposta nel DTO personalizzato, trattando i nullvalori come facoltativo.

Includere / rimuovere dinamicamente i campi dalle forze di risposta, comprese le condizioni aggiuntive sui client.


Ad ogni modo, in qualunque modo scegliate, assicuratevi di mantenerlo coerente e ben documentato. In questo modo non importa cosa usi per la tua API, a condizione che il comportamento sia prevedibile.


Sì, la stringa vuota è un valore che uso nullcome riferimento. Mescolare i valori con i riferimenti è una delle mie preoccupazioni. Come si differenziano i campi facoltativi nullda quelli di una stringa non facoltativa che hanno nullvalore? Per quanto riguarda l'analisi, la verifica dell'esistenza di proprietà non rende il parser più fragile?
imel96,

2
@ imel96 I campi non opzionali non possono MAI essere nulli. Se qualcosa non è facoltativo DEVE sempre contenere un valore (del suo tipo specifico).
Andy,

3
Questo. In quanto consumatore frequente di API, odio quando devo confrontarmi con strutture "dinamiche" che mi ritornano, anche se è sotto forma di un campo opzionale che viene omesso. (anche molto d'accordo sul fatto che c'è una grande differenza tra uno ZLS e uno Null). Accetterei felicemente valori nulli per tutto il giorno. Come autore dell'API, uno dei miei obiettivi è quello di rendere il consumo del cliente il più indolore possibile, e ciò significa avere sempre previste strutture di dati.
jleach

@DavidPacker quindi, se ho capito bene, usi nullper indicare che il valore è facoltativo. Quindi, quando si definisce un oggetto che ha una proprietà stringa non facoltativa e il consumatore non ha questa proprietà, deve inviare una stringa vuota per quella proprietà. È giusto?
imel96

2
@GregoryNisbet Non farlo, per favore. Non ha senso.
Andy,

3

null L'utilizzo dipende dall'applicazione / dalla lingua

In definitiva, la scelta se utilizzare o meno nullun valore applicativo valido è in gran parte determinata dall'applicazione e dal linguaggio / interfaccia / edge di programmazione.

A livello fondamentale, consiglierei di provare a usare tipi distinti se ci sono classi distinte di valori. nullpotrebbe essere un'opzione se la tua interfaccia lo consente e ci sono solo due classi di una proprietà che stai tentando di rappresentare. L'omissione di una proprietà può essere un'opzione se l'interfaccia o il formato lo consentono. Un nuovo tipo aggregato (classe, oggetto, tipo di messaggio) può essere un'altra opzione.

Per il tuo esempio di stringa, se questo è nel linguaggio di programmazione, mi farei un paio di domande.

  1. Ho intenzione di aggiungere futuri tipi di valori? In tal caso, Optionprobabilmente sarà meglio per la progettazione dell'interfaccia.
  2. Quando devo convalidare le chiamate dei consumatori? Staticamente? Dinamicamente? Prima? Dopo? Affatto? Se il tuo linguaggio di programmazione lo supporta, utilizza i vantaggi della digitazione statica poiché evita la quantità di codice che devi creare per la convalida. Optionprobabilmente riempie meglio questo caso se la stringa non era nulla. Tuttavia, probabilmente dovrai comunque verificare l'input dell'utente per un nullvalore stringa, quindi probabilmente rinvierei alla prima riga di domande: quanti tipi di valori fare / vorrò rappresentare.
  3. È nullindicativo di un errore del programmatore nel mio linguaggio di programmazione? Sfortunatamente, nullè spesso il valore predefinito per puntatori o riferimenti non inizializzati (o non esplicitamente inizializzati) in alcune lingue. È nullaccettabile un valore come valore predefinito? È sicuro come valore predefinito? A volte nullè indicativo di valori deallocati. Devo fornire ai consumatori della mia interfaccia un'indicazione di questi potenziali problemi di gestione della memoria o di inizializzazione nel loro programma? Qual è la modalità di fallimento di una tale chiamata di fronte a tali problemi? Il chiamante è nello stesso processo o thread del mio in modo che tali errori rappresentino un rischio elevato per la mia applicazione?

A seconda delle risposte a queste domande, probabilmente sarai in grado di capire se nullè giusto o meno per la tua interfaccia.

Esempio 1

  1. L'applicazione è fondamentale per la sicurezza
  2. Stai utilizzando un tipo di inizializzazione dell'heap all'avvio ed nullè un possibile valore di stringa restituito in caso di errore nell'allocazione dello spazio per una stringa.
  3. C'è un potenziale che una tale stringa colpisca la tua interfaccia

Risposta: nullprobabilmente non è appropriato

Motivazione: nullin questo caso viene effettivamente utilizzato per indicare due diversi tipi di valori. Il primo potrebbe essere un valore predefinito che l'utente della tua interfaccia potrebbe voler impostare. Sfortunatamente, il secondo valore è un flag per indicare che il sistema non funziona correttamente. In questi casi, probabilmente vuoi fallire nel modo più sicuro possibile (qualunque cosa ciò significhi per il tuo sistema).

Esempio 2

  1. Stai usando una C struct che ha un char *membro.
  2. Il tuo sistema non utilizza l'allocazione dell'heap e stai utilizzando il controllo MISRA.
  3. L'interfaccia accetta questa struttura come puntatore e verifica che la struttura non punti NULL
  4. Il valore predefinito e sicuro del char *membro per la tua API può essere indicato da un singolo valore diNULL
  5. Dopo l'inizializzazione della struttura dell'utente, si desidera offrire all'utente la possibilità di non inizializzare esplicitamente il char *membro.

Risposta: NULLpuò essere appropriato

Motivazione: C'è una piccola possibilità che la tua struttura passi il NULLcontrollo ma non è inizializzata. Tuttavia, l'API potrebbe non essere in grado di tenerne conto a meno che non si disponga di una sorta di checksum sul valore della struttura e / o sul controllo dell'intervallo dell'indirizzo della struttura. Le linter MISRA-C possono aiutare gli utenti dell'API segnalando l'utilizzo delle strutture prima della loro inizializzazione. Tuttavia, per quanto riguarda il char *membro, se il puntatore a struct punta a una struttura inizializzata, NULLè il valore predefinito di un membro non specificato in un inizializzatore di struttura. Pertanto, NULLpuò fungere da valore predefinito sicuro per il char *membro struct nell'applicazione.

Se si trova su un'interfaccia di serializzazione, mi porrei le seguenti domande sull'opportunità di utilizzare null su una stringa.

  1. È nullindicativo di un potenziale errore lato client? Per JSON in JavaScript, questo è probabilmente un no poiché nullnon è necessariamente utilizzato come indicazione di errore di allocazione. In JavaScript, viene utilizzato come indicazione esplicita dell'assenza di un oggetto da un riferimento da impostare in modo problematico. Tuttavia, esistono parser e serializzatori non javascript che associano JSON nullal nulltipo nativo . In questo caso, ciò porta alla discussione se l' nulluso nativo sia o meno corretto per la tua particolare combinazione di lingua, parser e serializzatore.
  2. L'assenza esplicita di un valore di proprietà influisce più di un singolo valore di proprietà? A volte a nullsta effettivamente indicando che hai un nuovo tipo di messaggio interamente. Potrebbe essere più pulito per i tuoi consumatori il formato di serializzazione semplicemente specificare un tipo di messaggio completamente diverso. Ciò garantisce che la loro convalida e la logica dell'applicazione possano avere una netta separazione tra le due distinzioni di messaggi fornite dalla tua interfaccia web.

Consiglio generale

nullnon può essere un valore su un bordo o un'interfaccia che non lo supporta. Se stai usando qualcosa di estremamente sciolto nella tipizzazione dei valori delle proprietà (ad es. JSON), prova a spingere qualche forma di schema o convalida sul software per i consumatori (ad es. JSON Schema ) se puoi. Se si tratta di un'API del linguaggio di programmazione, convalida l'immissione dell'utente se possibile (tramite la digitazione) o il livello di rumorosità che è ragionevole in fase di esecuzione (ovvero pratica la programmazione difensiva su interfacce rivolte al consumatore). Altrettanto importante, documentare o definire il bordo in modo che non ci siano dubbi su:

  • Che tipo (i) di valore accetta una determinata proprietà
  • Quali intervalli di valori sono validi per una determinata proprietà.
  • Come dovrebbe essere strutturato un tipo aggregato. Quali proprietà devono / dovrebbero / possono essere presenti in un tipo aggregato?
  • Se si tratta di un tipo di contenitore, quanti elementi possono o devono contenere il contenitore e quali sono i tipi di valori contenuti nel contenitore?
  • In quale ordine vengono restituite le proprietà o le istanze di un tipo contenitore o aggregati?
  • Quali sono gli effetti collaterali dell'impostazione di valori particolari e quali sono gli effetti collaterali della lettura di tali valori?

1

Ecco la mia analisi personale di queste domande. Non è supportato da alcun libro, carta, studio o altro, solo dalla mia esperienza personale.

Stringhe vuote come null

Questo è un no-go per me. Non mescolare la semantica della stringa vuota con quella della non definita. In molti casi possono essere perfettamente intercambiabili, ma puoi imbatterti in casi in cui non definiti e definiti ma vuoti significano qualcosa di diverso.

Una specie di stupido esempio: diciamo che esiste un attributo che memorizza una chiave esterna e che l'attributo non è definito o è null, ciò significherebbe che non esiste una relazione definita, mentre una stringa vuota ""potrebbe essere intesa come una relazione definita e il ID del record esterno è quella stringa vuota.

Non definito vs null

Questo non è un argomento in bianco o nero. Ci sono pro e contro per entrambi gli approcci.

A favore della definizione esplicita di nullvalori, ci sono questi professionisti:

  • I messaggi sono più auto-descrittivi in ​​quanto puoi conoscere tutte le chiavi semplicemente guardando qualsiasi messaggio
  • Relativamente al punto precedente, è più facile codificare e rilevare errori nel consumatore dei dati: è più facile rilevare errori se si recuperano le chiavi sbagliate (forse errori di ortografia, forse l'API è cambiata, ecc.).

A favore di assumere una chiave inesistente equivale alla semantica di null:

  • Alcuni cambiamenti sono più facili da accogliere. Ad esempio, se una nuova versione dello schema dei messaggi include una nuova chiave, è possibile codificare il consumatore delle informazioni per lavorare con questa chiave futura, anche quando il produttore del messaggio non è stato aggiornato e non fornisce ancora queste informazioni.
  • I messaggi possono essere meno dettagliati o più brevi

Nel caso in cui l'API sia in qualche modo stabile e la documenti accuratamente, penso che sia perfettamente OK affermare che una chiave inesistente è uguale al significato di null. Ma se è più disordinato e caotico (come spesso accade), penso che puoi evitare mal di testa se definisci esplicitamente ogni valore in ogni messaggio. Vale a dire in caso di dubbio, tendo a seguire l'approccio dettagliato.

Detto questo, la cosa più importante: dichiarare chiaramente le proprie intenzioni ed essere coerenti. Non fare una cosa qui e l'altra lì. Il software prevedibile è un software migliore.


L'esempio per l'utilizzo di stringhe vuote è ciò che intendo per dettagli di implementazione, ovvero supponendo che l'API venga utilizzata per esporre le righe del database. Farebbe differenza se non ci sono database coinvolti e puramente per il trasferimento di rappresentazioni di oggetti?
imel96,

Non deve essere un dettaglio di implementazione. Il mio esempio in realtà parla di PK che sono correlati al DB, ma quello che ho cercato di spiegare è che una stringa vuota non è nulla / niente / null. Un altro esempio: in un gioco c'è un oggetto personaggio e ha un attributo "partner". Un nullpartner significa chiaramente che non esiste alcun partner, ma ""può essere compreso in quanto esiste un partner il cui nome è "".
bgusach,

Sto bene con riferimento a partner nullo significa che non c'è partner e anche il riferimento non è una stringa. Ma il nome del partner è una stringa, anche se consenti null come nome del partner, non prenderesti quel null e lo sostituiresti con una stringa vuota ad un certo punto?
imel96,

Se non esiste un partner, non cambierei nullper una stringa vuota. Forse renderlo in un modulo, ma mai nel modello di dati.
bgusach,

Non intendevo nessun partner, il partner sarebbe un oggetto. È il partner di namecui stavo parlando, vorresti che il nome del partner fosse nullo?
imel96

1

Fornirei una stringa vuota in una situazione in cui è presente una stringa e sembra essere una stringa vuota. Vorrei fornire null in una situazione in cui voglio dire esplicitamente "no, questi dati non sono lì". E ometti la chiave per dire "nessun dato lì, non preoccuparti".

Giudichi quale di queste situazioni può accadere. Ha senso che l'applicazione abbia stringhe vuote? Vuoi distinguere tra dire esplicitamente "nessun dato" usando null e implicitamente senza avere valore? Dovresti avere entrambe le possibilità (null e nessuna chiave presente) se entrambe devono essere distinte dal cliente.

Ora tieni presente che si tratta solo di trasmettere dati. Ciò che il destinatario fa con i dati è la loro attività e faranno ciò che è più conveniente per loro. Il destinatario dovrebbe essere in grado di gestire tutto ciò che gli viene lanciato (eventualmente rifiutando i dati) senza arresti anomali.

Se non ci sono altre considerazioni, trasmetterei ciò che è più conveniente per il mittente e lo documenterei . Preferirei non inviare affatto valori assenti perché questo probabilmente migliorerà la velocità di codifica, trasmissione e analisi di JSON.


Mi piace il tuo punto in "se il cliente deve distinguerlo".
imel96

0

Anche se non posso dire ciò che è meglio non è quasi certamente un semplice dettaglio di implementazione , cambia la struttura di come puoi interagire con quella variabile.

Se qualcosa può essere nullo , dovresti sempre trattarlo come se fosse nullo ad un certo punto , quindi avrai sempre due flussi di lavoro , uno per null, uno per una stringa valida. Un flusso di lavoro diviso non è necessariamente una cosa negativa, poiché c'è un po 'di gestione degli errori e usi di casi speciali che potresti utilizzare, ma offusca il tuo codice.

Se interagisci sempre con la stringa nello stesso modo , probabilmente sarà più facile per la funzionalità rimanere nella tua testa .

Come per qualsiasi domanda "qual è la cosa migliore", mi rimane la risposta: dipende . Se si desidera dividere il flusso di lavoro e acquisire in modo più esplicito quando qualcosa non è impostato, utilizzare null. Se preferisci che il programma continui a fare così, usa una stringa vuota. L'importante è essere coerenti , scegliere un ritorno comune e attenersi a quello.

Considerando che stai creando un'API, consiglierei di attenersi a una stringa vuota poiché c'è meno per l'utente di compensare, perché come utente di un'API non saprò tutti i motivi per cui la tua API potrebbe darmi un valore nullo a meno che tu ' sono molto ben documentati, che alcuni utenti non leggeranno comunque.


Avere un "flusso di lavoro diviso" è male. Diciamo che dal lato produttore tutto è pulito, i metodi di tipo stringa restituiscono solo strings, mai null. Se l'API utilizza null, a un certo punto il produttore deve crearlo nullper conformarsi all'API. Quindi anche il consumatore deve gestire null. Ma penso di avere quello che stai dicendo, basta decidere e definire l'API con autorità, giusto? Significa che non c'è niente di sbagliato in nessuno di loro?
imel96

Sì, qualunque cosa tu faccia nella tua API avrà un impatto sul modo in cui l'utente dovrà strutturare il proprio codice, quindi considerando il tuo progetto in termini di utente dell'API dovresti essere in grado di definire quale sia il modo migliore. In definitiva è la tua API. Sii coerente. Solo tu puoi decidere i pro e i contro dell'approccio.
Erdrik Ironrose,

0

Documento!

TL; DR>

Fai come ritieni opportuno - a volte il contesto in cui viene utilizzato è importante. Esempio, associare le variabili a un Oracle SQL: la stringa vuota viene interpretata come NULL.

Semplicemente direi: assicurati di documentare ogni scenario menzionato

  • NULLO
  • Vuoto (vuoto)
  • Mancante (rimosso)

Il tuo codice potrebbe agire in diversi modi: documenta come il tuo codice reagisce ad esso:

  • Fallito (eccezione ecc.), Forse non riesce nemmeno la convalida (possibilmente un'eccezione controllata) vs non è in grado di gestire correttamente la situazione (NullPointerException).
  • Fornire impostazioni predefinite ragionevoli
  • Il codice si comporta diversamente

Inoltre, spetta a te comportarti in modo coerente e possibilmente adottare alcune delle tue migliori pratiche. Documentare quel comportamento coerente. Esempi:

  • Tratta Null e Missing the same
  • Tratta una stringa vuota esattamente come tale. Solo nel caso di un bind SQL, potrebbe essere considerato un vuoto. Assicurati che il tuo SQL si comporti in modo coerente e nel modo previsto.

Il problema è che, senza affrontare le preoccupazioni, il disaccordo si è verificato abbastanza spesso. Considerare nell'ambiente di squadra, la decisione deve essere una decisione di squadra, molte volte ciò significa che ci sarebbe una discussione. Quando hai più squadre, ogni squadra ha diritto alle proprie decisioni. Ho visto API che posso solo immaginare che siano implementate da diversi team che non sono d'accordo tra loro. Se qualcuno può essere d'accordo su una cosa, documentarla è banale.
imel96,

0

tl; dr - se lo usi: sii coerente con ciò che significa.

Se includessi null, cosa significherebbe? C'è un universo di cose che cosa potrebbe significare. Un valore non è semplicemente sufficiente per rappresentare un valore mancante o sconosciuto (e queste sono solo due delle innumerevoli possibilità: ad esempio mancante - è stato misurato, ma non lo sappiamo ancora. Sconosciuto - non abbiamo provato a misurare it.)

In un esempio in cui mi sono imbattuto di recente, un campo potrebbe essere vuoto perché non è stato segnalato per proteggere la privacy di qualcuno, ma era noto dal lato del mittente, non noto dal lato del mittente, ma noto al giornalista originale o sconosciuto a entrambi. E tutto ciò contava per il destinatario. Quindi di solito un valore non è sufficiente.

Con un'ipotesi del mondo aperto (semplicemente non sai di cose non dichiarate), lo lasceresti fuori e potrebbe essere qualsiasi cosa. Con l'ipotesi del mondo chiuso (le cose non dichiarate sono false, ad esempio in SQL) è meglio chiarire cosa nullsignifica ed essere coerenti con quella definizione come si può ...

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.