DEV Community

Cover image for Validate your data structures with Vine in your Dart projects
Baptiste Parmantier
Baptiste Parmantier

Posted on

Validate your data structures with Vine in your Dart projects

Origins

Vine is a data validation library developed by Harminder Virk for the Adonis framework.

He was originally an integral part of the framework but was moved to an agnostic package when version 6 was released.

From the documentation available here.

VineJS is a form data validation library for Node.js. You may use it to validate the HTTP request body in your backend applications.

  • VineJS is one of the fastest validation libraries in the Node.js ecosystem

  • It provides both runtime and static type safety

  • Built for validating form data and JSON payloads

  • Has first-class support for defining custom error messages and formatting errors

  • Comes with an extensive suite of 50+ validation rules and 12 schema types

  • Extensible. You can add custom rules and schema types to VineJS. We also make it super easy to test custom validation rules

import vine from '@vinejs/vine'

const schema = vine.object({
  email: vine.string().email(),
  password: vine
    .string()
    .minLength(8)
    .maxLength(32)
    .confirmed()
})

await vine.validate({ schema, data: {...} })

The validation component is essential if you want to have total control over every piece of data entering your backend application.

Never trust your users

It is mainly used to validate 2 data sources.

  1. The contents of the body in the case of POST, PUT or PATCH methods.
  2. Url parameters such as a search property allowing additional behaviour to an SQL query or pagination (limit, page).
const schema = vine.compile(
  vine.object({
    title: vine.string().minLength(3).maxLength(255),
    status: vine.enum(['draft', 'published'])
    content: vine.string().minLength(8)
  })
)

export default class Controller {
  async store({ request }: HttpContext): Promise<Articles> {
    const data = request.validateUsing(schema)
    return Article.create(data)
  }
}
Enter fullscreen mode Exit fullscreen mode

Dart ecosystem overview

The Dart ecosystem is best known for its Flutter cross-platform framework, but the language can also be used in other contexts, such as for web applications (SPA, SSR, static), microservices (although the quality of the packages associated with classic protocols such as AMQP is debatable) or even command guests...

The notion of data validation is very little present in the ecosystem, in fact there are mainly two big leaders for Flutter (simply the tool provided by the framework or form_validation).
In the context of Dart, we can see validators downloaded 107M times this week, but this package has not been updated for 3 years at the time of writing.

The validators package has no fluent API or more complex validation, it's more of a helpers library.

On a purely personal level, and although I appreciate the technology, I find it extremely simplistic to sum up the Dart language as simply Flutter. The language itself is more than sufficient to build almost any application; it meets a need for performance, high loads and, like Javascript, is extremely versatile (backend, mobile, desktop, web, etc...).

Following this guideline, I wanted to use my last 10 years of experience with the Adonis framework to enrich the Dart language ecosystem.

Project portability request

To this end, I asked the package's original creator if I could reuse the name of his project in order to produce a quality library for all the validation needs of any type of application.

Vine in Dart

Like its namesake, this package enables validation operations to be performed on a more or less complex data structure.

View on Github View on Dart Pub

To date, we support an average of 29,000,000 validation operations per second. Benchmarks are available here.

Schema 101

The design of a validation schema follows a set of rules that allow us to structure our validation as well as possible, while avoiding having to continue going through the rules if one of them is not respected, so we can observe several levels of validation.

Description of the rules in schema 101.
Schema 101

A validation schema systematically begins with a form, which must be an object.

final schema = vine.object({...});
Enter fullscreen mode Exit fullscreen mode

To this base, we can add a first property to be validated, for this example we will define a username.

final schema = vine.object({
  'username': vine.string()
});
Enter fullscreen mode Exit fullscreen mode

Now that our property has been declared, we're going to add some additional validation rules (often in line with your column type constraints when validating data before an SQL operation).

final schema = vine.object({
  'username': vine.string()
    .minLength(3)
    .maxLength(255) // 👈 In according to VARCHAR(255) type
});
Enter fullscreen mode Exit fullscreen mode

We're now going to add an extra property which may or may not be present in the payload. To add this last constraint, we'll use a modifier.

final schema = vine.object({
  'username': vine.string()
    .minLength(3)
    .maxLength(255),
  'age': vine.number().optional()
});
Enter fullscreen mode Exit fullscreen mode

Our current scheme now meets the following tests.

void main() {
  final schema = vine.object({
    'username': vine.string()
      .minLength(3)
      .maxLength(255),
    'age': vine.number().optional()
  });

  test('should be valid', () {
    final payload1 = {'username': 'john'};
    expect(() => vine.validate(payload1, schema), returnsNormally);

    final payload2 = {'username': 'John', 'age': 28};
    expect(() => vine.validate(payload2, schema), returnsNormally);
  });

  test('should be invalid', () {
    final payload = {'username': 'jo'};
    expect(() => vine.validate(payload, schema), throwsA(isA<ValidationException>()));
  });
}
Enter fullscreen mode Exit fullscreen mode

Compiling your schema

In our previous example we designed a simple validation schema, but this is not reusable and must be rewritten for each validation operation.

To avoid having to declare your schema N times, the package allows you to transform your dynamic schema into a static schema using a compilation operation.

final validator = vine.compile(
  vine.object({
    'username': vine.string()
      .minLength(3)
      .maxLength(255),
    'age': vine.number().optional()
  });
);

final data = validator.validate({...});
print(data);
Enter fullscreen mode Exit fullscreen mode

It is preferable to transfer your compiled schema to a file dedicated to validators in order to initialise them at the boot time of your application.

Schema

Examples of how to use the package are available in the test/ folder in the repository.

String schema
Character strings are a complex subject because they can represent a multitude of use cases (emails, phone number, etc...).
The validation rules provided will enable you to refine your constraints.
final schema = vine.object({
  'property': vine.string()
    .minLength(3)
    .maxLength(255)
    .fixedLength(4)
    .email()
    .phone()
    .ipAddress()
    .url()
    .alpha()
    .alphaNumeric()
    .startsWith()
    .endsWith()
    .confirmed()
    .trim()
    .normalizeEmail(lowercase: true)
    .toUpperCase()
    .toLowerCase()
    .toCamelCase()
    .uuid()
    .isCreditCard()
    .sameAs('otherProperty')
    .notSameAs('otherProperty')
    .inList(['firstValue', 'secondValue'])
    .notInList(['thirdValue'])
    .regex(...)
    .requiredIfExist(['otherProperty'])
    .requiredIfAnyExist(['otherProperty'])
    .requiredIfMissing(['otherProperty'])
    .requiredIfAnyMissing(['otherProperty'])
    .nullable()
    .optional()
    .transform((ctx, field) => 'foo');
});
Enter fullscreen mode Exit fullscreen mode

Number schema
Numbers are initially expressed using the num language type. You can refine int or double using one of the available modifiers.
final schema = vine.object({
  'property': number()
    .range([1, 2, 3])
    .min(1)
    .max(3)
    .negative()
    .positive()
    .double()
    .integer()
    .requiredIfExist(['otherProperty'])
    .requiredIfAnyExist(['otherProperty'])
    .requiredIfMissing(['otherProperty'])
    .requiredIfAnyMissing(['otherProperty'])
    .nullable()
    .optional()
    .transform((ctx, field) => 1);
});
Enter fullscreen mode Exit fullscreen mode

Boolean schema
This scheme supports various inputs such as 0, 1, on, off, true or false.
You can apply or remove this behaviour to retain Boolean support only.
final schema = vine.object({
  'property': vine.boolean()
    .requiredIfExist(['otherProperty'])
    .requiredIfAnyExist(['otherProperty'])
    .requiredIfMissing(['otherProperty'])
    .requiredIfAnyMissing(['otherProperty'])
    .nullable()
    .optional()
    .transform((ctx, field) => true);
});
Enter fullscreen mode Exit fullscreen mode

Date schema
Dates are interpreted from a String or directly from a DateTime instance.
final schema = vine.object({
  'property': date()
    .before(DateTime.now())
    .after(DateTime.now())
    .between(
      DateTime.now().subtract(Duration(days: 1)), 
      DateTime.now().add(Duration(days: 1))
    )
    .beforeField('otherProperty')
    .afterField('otherProperty')
    .betweenFields('firstProperty', 'secondProperty')
    .requiredIfExist(['otherProperty'])
    .requiredIfAnyExist(['otherProperty'])
    .requiredIfMissing(['otherProperty'])
    .requiredIfAnyMissing(['otherProperty'])
    .nullable()
    .optional()
    .transform((ctx, field) => DateTime.now());
});
Enter fullscreen mode Exit fullscreen mode

Union schema
Union allows you to assume several input value types without losing control over the expected final type.
final schema = vine.object({
  'property': vine.union([
      vine.string(),
      vine.number()
    ])
    .requiredIfExist(['otherProperty'])
    .requiredIfAnyExist(['otherProperty'])
    .requiredIfMissing(['otherProperty'])
    .requiredIfAnyMissing(['otherProperty'])
    .nullable()
    .optional()
    .transform((ctx, field) => 'foo');
});
Enter fullscreen mode Exit fullscreen mode

Array schema
This type of schema is used to control the contents of an array of elements by forcing a specific type of data.
final schema = vine.object({
  'property': vine.array(vine.string())
    .requiredIfExist(['otherProperty'])
    .requiredIfAnyExist(['otherProperty'])
    .requiredIfMissing(['otherProperty'])
    .requiredIfAnyMissing(['otherProperty'])
    .nullable()
    .optional()
    .transform((ctx, field) => []);
});
Enter fullscreen mode Exit fullscreen mode

Object schema
The basis of any validation scheme, the object is used to validate complex structures, even nested ones.
final schema = vine.object({
  'property': object({...})
    .requiredIfExist(['otherProperty'])
    .requiredIfAnyExist(['otherProperty'])
    .requiredIfMissing(['otherProperty'])
    .requiredIfAnyMissing(['otherProperty'])
    .nullable()
    .optional()
    .transform((ctx, field) => 'foo');
});
Enter fullscreen mode Exit fullscreen mode

Enum schema
This scheme enables stricter validation of values by making them contractual to a strict definition.
enum MyEnum implements VineEnumerable<String> {
  first('first'),
  second('second');

  final String value;
  const MyEnum(this.value);
}

final schema = vine.object({
  'property': vine.enumerate(MyEnum.values)
    .requiredIfExist(['otherProperty'])
    .requiredIfAnyExist(['otherProperty'])
    .requiredIfMissing(['otherProperty'])
    .requiredIfAnyMissing(['otherProperty'])
    .nullable()
    .optional()
    .transform((ctx, field) => MyEnum.first.value);
});
Enter fullscreen mode Exit fullscreen mode

Any schema
This type of scheme allows any input value, but we recommend that you use vine.union([...]).
final schema = vine.object({
  'property': vine.any()
    .requiredIfExist(['otherProperty'])
    .requiredIfAnyExist(['otherProperty'])
    .requiredIfMissing(['otherProperty'])
    .requiredIfAnyMissing(['otherProperty'])
    .nullable()
    .optional()
    .transform((ctx, field) => 'foo');
});
Enter fullscreen mode Exit fullscreen mode


OpenApi

To make your validation schemas universal, the package implements a reporter which you can use to export to the OpenApi specification.

final schema = vine.object({
  'stringField': vine.string().minLength(3).maxLength(20),
  'emailField': vine.string().email(),
  'numberField': vine.number().min(18).max(100),
  'booleanField': vine.boolean(),
  'enumField': vine.enumerate(MyEnum.values),
  'arrayField': vine.array(vine.string()).minLength(1),
  'unionField': vine.union([
    vine.string().minLength(3).maxLength(20),
    vine.number().min(10).max(20),
  ])
});

final report = vine.openApi.report(schemas: {'MySchema': schema}); 
print(reporter);
Enter fullscreen mode Exit fullscreen mode

Acknowledgements

I would like to thank Harminder Virk for all his work, which has enabled me to produce a clean, efficient and user friendly package, but above all to use the name and identity of the creation.

Top comments (0)